feat(kis-collection): finalize sqlite migration, add fallback resilience, and update WBS documentation
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "Temp" / "document_search_index_v1.json"
|
||||
EXCLUDED_PREFIXES = ("docs/archive/", "suggest/", "artifacts/archive/")
|
||||
INCLUDED_ROOTS = ("docs", "spec", "governance", "src", "tools", "AGENTS.md", "README.md")
|
||||
|
||||
|
||||
def _is_excluded(rel: str) -> bool:
|
||||
return rel.startswith(EXCLUDED_PREFIXES)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
indexed: list[str] = []
|
||||
excluded: list[str] = []
|
||||
|
||||
for path in ROOT.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if _is_excluded(rel):
|
||||
excluded.append(rel)
|
||||
continue
|
||||
if rel.startswith("docs/") or rel.startswith("spec/") or rel.startswith("governance/") or rel.startswith("src/") or rel.startswith("tools/") or rel in {"AGENTS.md", "README.md"}:
|
||||
indexed.append(rel)
|
||||
|
||||
result = {
|
||||
"formula_id": "DOCUMENT_SEARCH_INDEX_V1",
|
||||
"gate": "PASS",
|
||||
"indexed_count": len(indexed),
|
||||
"excluded_count": len(excluded),
|
||||
"excluded_prefixes": list(EXCLUDED_PREFIXES),
|
||||
"indexed_sample": sorted(indexed)[:50],
|
||||
"excluded_sample": sorted(excluded)[:50],
|
||||
}
|
||||
OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -28,6 +28,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# 입력 파일
|
||||
PREDICTION_ACCURACY = ROOT / "Temp" / "prediction_accuracy_harness_v2.json"
|
||||
REBOUND_EFF = ROOT / "Temp" / "rebound_sell_efficiency_v1.json"
|
||||
LATE_CHASE = ROOT / "Temp" / "late_chase_attribution_v1.json"
|
||||
PROPOSAL_HIS = ROOT / "Temp" / "proposal_evaluation_history.json"
|
||||
@@ -46,6 +47,30 @@ def load_json(p: Path) -> dict | list:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_prediction_accuracy() -> dict:
|
||||
data = load_json(PREDICTION_ACCURACY)
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def current_t5_status() -> tuple[float | None, str]:
|
||||
"""WBS-7.2 source-of-truth shim.
|
||||
|
||||
Prefer the latest prediction accuracy harness when present. Do not fall back to
|
||||
stale hardcoded percentages when the harness explicitly says sample=0.
|
||||
"""
|
||||
data = load_prediction_accuracy()
|
||||
if not data:
|
||||
return None, "ARTIFACT_MISSING"
|
||||
|
||||
t5_sample = int(data.get("t5_sample") or 0)
|
||||
t5_rate = data.get("t5_op_rate")
|
||||
if t5_sample == 0:
|
||||
return None, "DATA_GATED"
|
||||
if isinstance(t5_rate, (int, float)):
|
||||
return float(t5_rate), "OK"
|
||||
return None, "DATA_MISSING"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rebound = load_json(REBOUND_EFF)
|
||||
chase = load_json(LATE_CHASE)
|
||||
@@ -90,7 +115,8 @@ def main() -> int:
|
||||
})
|
||||
|
||||
# ── (3) T+1 / T+5 KPI 추적 ─────────────────────────────────────────
|
||||
# operational_report 에서 일치율 추출
|
||||
# operational_report는 보고서 텍스트용 보조 원장이고,
|
||||
# T+5 현재값은 prediction_accuracy_harness_v2.json을 우선한다.
|
||||
t1_rate = None
|
||||
t5_rate = None
|
||||
sections = op.get("sections", []) if isinstance(op, dict) else []
|
||||
@@ -109,7 +135,11 @@ def main() -> int:
|
||||
|
||||
# 직접 알려진 값 사용 (operational_report 에서 확인된 수치)
|
||||
if t1_rate is None: t1_rate = 47.28
|
||||
if t5_rate is None: t5_rate = 35.86
|
||||
live_t5_rate, live_t5_status = current_t5_status()
|
||||
if live_t5_rate is not None:
|
||||
t5_rate = live_t5_rate
|
||||
elif t5_rate is None:
|
||||
t5_rate = None
|
||||
|
||||
kpi_tracker.append({
|
||||
"metric": "T+1_match_rate_pct",
|
||||
@@ -119,14 +149,24 @@ def main() -> int:
|
||||
"status": "BELOW_TARGET" if t1_rate < 55.0 else "ON_TARGET",
|
||||
"note": "동전던지기(50%) 이하 — 신호 품질 개선 필요",
|
||||
})
|
||||
kpi_tracker.append({
|
||||
"metric": "T+5_match_rate_pct",
|
||||
"current": t5_rate,
|
||||
"target_min": 55.0,
|
||||
"gap": round(55.0 - t5_rate, 2),
|
||||
"status": "BELOW_TARGET" if t5_rate < 55.0 else "ON_TARGET",
|
||||
"note": "T+5 35.86% — ANTI_LATE_ENTRY_GATE_V2 임계값 보정 시 개선 목표",
|
||||
})
|
||||
if t5_rate is None:
|
||||
kpi_tracker.append({
|
||||
"metric": "T+5_match_rate_pct",
|
||||
"current": None,
|
||||
"target_min": 55.0,
|
||||
"gap": None,
|
||||
"status": "DATA_GATED",
|
||||
"note": f"T+5 current source={live_t5_status} — sample=0 or artifact missing; do not cite stale 35.86%",
|
||||
})
|
||||
else:
|
||||
kpi_tracker.append({
|
||||
"metric": "T+5_match_rate_pct",
|
||||
"current": t5_rate,
|
||||
"target_min": 55.0,
|
||||
"gap": round(55.0 - t5_rate, 2),
|
||||
"status": "BELOW_TARGET" if t5_rate < 55.0 else "ON_TARGET",
|
||||
"note": "T+5 current source-of-truth read from prediction_accuracy_harness_v2.json",
|
||||
})
|
||||
|
||||
# ── (4) OUTCOME_TRUST_GAP ───────────────────────────────────────────
|
||||
# design_score 97.12 vs 실측 T+5 35.86% 간 신뢰도 괴리
|
||||
@@ -134,7 +174,8 @@ def main() -> int:
|
||||
"design_score": rb_score,
|
||||
"actual_t5_pct": t5_rate,
|
||||
"gap_note": (
|
||||
f"설계점수 rebound_efficiency={rb_score:.2f} vs 실측 T+5 일치율 {t5_rate}% — "
|
||||
f"설계점수 rebound_efficiency={rb_score:.2f} vs 실측 T+5 일치율 "
|
||||
f"{('DATA_GATED' if t5_rate is None else f'{t5_rate}%')} — "
|
||||
f"설계점수가 높아도 실제 수익성 지표(T+5)는 낮을 수 있음. "
|
||||
f"두 지표를 항상 물리적으로 분리해 표시해야 한다."
|
||||
),
|
||||
@@ -153,11 +194,14 @@ def main() -> int:
|
||||
print(f"\n [T+1/T+5 KPI 현황]")
|
||||
for k in kpi_tracker:
|
||||
status_icon = "✗" if k["status"] == "BELOW_TARGET" else "✓"
|
||||
print(f" {k['metric']}: {k['current']}% (목표 ≥{k['target_min']}%) {status_icon}")
|
||||
if k["current"] is None:
|
||||
print(f" {k['metric']}: DATA_GATED (목표 ≥{k['target_min']}%) {status_icon}")
|
||||
else:
|
||||
print(f" {k['metric']}: {k['current']}% (목표 ≥{k['target_min']}%) {status_icon}")
|
||||
print(f" → {k['note']}")
|
||||
|
||||
print(f"\n [보정루프 개선 경로]")
|
||||
print(f" T+5 35.86% → 50%+ 목표:")
|
||||
print(f" T+5 {'DATA_GATED' if t5_rate is None else f'{t5_rate}%'} → 50%+ 목표:")
|
||||
print(f" Step 1. ALEG_V2_GATE1_BLOCK_PCT(3%) → 표본 누적 후 최적값 보정")
|
||||
print(f" Step 2. DSD_V1 가중치 → logistic regression 최적화")
|
||||
print(f" Step 3. K2 분할비율 0.5 → 30/70/40/60/50/50 backtest 비교")
|
||||
@@ -191,7 +235,7 @@ def main() -> int:
|
||||
"correction_steps": [
|
||||
f"rebound_efficiency_score={rb_score:.2f} → 보고서 표시 시 [UNVALIDATED_DESIGN_SCORE: n={rb_combo}] 주석 필수",
|
||||
f"late_chase_attribution: samples=0 → 최소 {SAMPLE_MIN}건 표본 누적 후 chase_entry_rate 검증",
|
||||
f"T+5 {t5_rate}% → 보정루프(calibration_registry.yaml) 기반 임계값 최적화로 50%+ 목표",
|
||||
f"T+5 {'DATA_GATED' if t5_rate is None else f'{t5_rate}%'} → 보정루프(calibration_registry.yaml) 기반 임계값 최적화로 50%+ 목표",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
TEMP = ROOT / "Temp"
|
||||
|
||||
PLACEHOLDERS = {
|
||||
"breakout_failure_stop_v1.json": "DATA_MISSING",
|
||||
"consecutive_streak_v1.json": "DATA_MISSING",
|
||||
"execution_capacity_ladder_v1.json": "DATA_MISSING",
|
||||
"execution_plan_compiler_v1.json": "DATA_MISSING",
|
||||
"fifty_two_week_high_trigger_v1.json": "DATA_MISSING",
|
||||
"golden_cross_signal_v1.json": "DATA_MISSING",
|
||||
"immutable_decision_ledger_v1.json": "DATA_MISSING",
|
||||
"model_governance_kill_switch_v1.json": "DATA_MISSING",
|
||||
"portfolio_transition_optimizer_v1.json": "DATA_MISSING",
|
||||
"scenario_shock_matrix_v1.json": "DATA_MISSING",
|
||||
"sector_exposure_graph_v1.json": "DATA_MISSING",
|
||||
"strong_close_signal_v1.json": "DATA_MISSING",
|
||||
"trend_filter_gate_v1.json": "DATA_MISSING",
|
||||
"volatility_expansion_breakout_v1.json": "DATA_MISSING",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
TEMP.mkdir(parents=True, exist_ok=True)
|
||||
for name, value in PLACEHOLDERS.items():
|
||||
path = TEMP / name
|
||||
payload = {
|
||||
"formula_id": path.stem.upper(),
|
||||
"gate": "DATA_MISSING",
|
||||
"status": "DATA_MISSING",
|
||||
"value": value,
|
||||
"note": "harness update required",
|
||||
}
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"wrote {path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,7 +1,7 @@
|
||||
"""qualitative_sell_strategy_v1 입력 ctx 조립 오케스트레이터.
|
||||
|
||||
데이터 출처 (2026-06-21 세션 실측 기준, KIS Open API 연동 이후):
|
||||
- relative_return_20d, volume_ratio_5d ← tools/fetch_naver_market_data_v1.py (무인증, 동작 확인)
|
||||
데이터 출처 (2026-06-22 기준, KIS Open API 우선):
|
||||
- relative_return_20d, volume_ratio_5d ← KIS Open API 우선, Naver는 fallback
|
||||
- sector_export_trend ← tools/fetch_trade_statistics_motie_v1.py (--csv 경로 권장)
|
||||
- short_turnover_share ← [신규] KIS Open API daily-short-sale(FHPST04830000)
|
||||
output2.ssts_vol_rlim — 실측 동작 확인(실전계좌 도메인,
|
||||
@@ -81,6 +81,125 @@ def _parse_date(value: str | None) -> dt.date | None:
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_price_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
normalized.append(
|
||||
{
|
||||
"date": str(row.get("date") or "").strip(),
|
||||
"close": row.get("close"),
|
||||
"open": row.get("open"),
|
||||
"high": row.get("high"),
|
||||
"low": row.get("low"),
|
||||
"volume": row.get("volume"),
|
||||
}
|
||||
)
|
||||
return [row for row in normalized if row["date"]]
|
||||
|
||||
|
||||
def _parse_kis_price_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for key in ("output2", "output1", "output"):
|
||||
items = payload.get(key)
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
date = str(
|
||||
item.get("stck_bsop_date")
|
||||
or item.get("data_date")
|
||||
or item.get("trd_dd")
|
||||
or item.get("date")
|
||||
or ""
|
||||
).strip()
|
||||
close = item.get("stck_clpr") or item.get("close") or item.get("price")
|
||||
volume = item.get("acml_vol") or item.get("volume") or item.get("trd_vol") or 0
|
||||
if not date:
|
||||
continue
|
||||
try:
|
||||
close_val = float(str(close).replace(",", ""))
|
||||
except Exception:
|
||||
close_val = 0.0
|
||||
try:
|
||||
volume_val = float(str(volume).replace(",", ""))
|
||||
except Exception:
|
||||
volume_val = 0.0
|
||||
rows.append(
|
||||
{
|
||||
"date": date.replace(".", "-"),
|
||||
"close": close_val,
|
||||
"open": float(str(item.get("stck_oprc") or item.get("open") or close or 0).replace(",", "")) if str(item.get("stck_oprc") or item.get("open") or close or 0).replace(",", "").strip() else close_val,
|
||||
"high": float(str(item.get("stck_hgpr") or item.get("high") or close or 0).replace(",", "")) if str(item.get("stck_hgpr") or item.get("high") or close or 0).replace(",", "").strip() else close_val,
|
||||
"low": float(str(item.get("stck_lwpr") or item.get("low") or close or 0).replace(",", "")) if str(item.get("stck_lwpr") or item.get("low") or close or 0).replace(",", "").strip() else close_val,
|
||||
"volume": volume_val,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def fetch_price_history_kis(code: str, kis_account: str | None, benchmark_code: str | None = None) -> dict[str, Any]:
|
||||
if not kis_account:
|
||||
return {"status": "DATA_MISSING", "rows": []}
|
||||
from src.quant_engine.kis_api_client_v1 import KisCredentials, get_daily_item_chart_price
|
||||
|
||||
try:
|
||||
creds = KisCredentials.load(kis_account)
|
||||
except RuntimeError as exc:
|
||||
return {"status": "DATA_MISSING", "rows": [], "error": str(exc)}
|
||||
|
||||
try:
|
||||
today = dt.date.today()
|
||||
end = today.strftime("%Y%m%d")
|
||||
start = (today - dt.timedelta(days=40)).strftime("%Y%m%d")
|
||||
payload = get_daily_item_chart_price(creds, code, start, end, period="D")
|
||||
rows = _parse_kis_price_rows(payload)
|
||||
if benchmark_code is not None and not rows:
|
||||
return {"status": "DATA_MISSING", "rows": []}
|
||||
if rows:
|
||||
return {
|
||||
"status": "OK",
|
||||
"rows": rows,
|
||||
"source_url": "KIS Open API /uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice",
|
||||
"source_as_of": _kst_now_iso(),
|
||||
}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"status": "DATA_MISSING", "rows": [], "error": str(exc)}
|
||||
return {"status": "DATA_MISSING", "rows": []}
|
||||
|
||||
|
||||
def _fetch_price_bundle(
|
||||
code: str,
|
||||
*,
|
||||
kis_account: str | None,
|
||||
prefer_kis: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""가격 히스토리와 벤치마크 히스토리를 동일 규칙으로 조립한다.
|
||||
|
||||
SRP:
|
||||
- 소스 선택은 이 함수가 담당
|
||||
- 상대수익률/거래량 비율 계산은 계산 함수가 담당
|
||||
- 호출자(process_one)는 결과만 소비한다
|
||||
"""
|
||||
kis_price = fetch_price_history_kis(code, kis_account)
|
||||
if prefer_kis and kis_price.get("status") == "OK":
|
||||
return {
|
||||
"source": "kis_open_api",
|
||||
"price": kis_price,
|
||||
}
|
||||
|
||||
session = _session()
|
||||
naver_price = fetch_price_history(session, code)
|
||||
source = "naver_finance" if naver_price.get("status") == "OK" else "data_missing"
|
||||
return {
|
||||
"source": source,
|
||||
"price": naver_price,
|
||||
"kis_price": kis_price,
|
||||
}
|
||||
|
||||
|
||||
def load_short_interest_csv(path: Path, code: str) -> dict[str, Any]:
|
||||
"""KRX 공매도종합포털 수동 다운로드 CSV. 컬럼: 종목코드, 잔고율, 잔고율변화20일, 거래비중."""
|
||||
import csv
|
||||
@@ -145,12 +264,15 @@ def build_ctx_for_ticker(
|
||||
external_context: dict[str, Any],
|
||||
kis_account: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
session = _session()
|
||||
price = fetch_price_history(session, code)
|
||||
benchmark = fetch_price_history(session, benchmark_code)
|
||||
price_bundle = _fetch_price_bundle(code, kis_account=kis_account, prefer_kis=True)
|
||||
benchmark_bundle = _fetch_price_bundle(benchmark_code, kis_account=kis_account, prefer_kis=True)
|
||||
price = price_bundle["price"]
|
||||
benchmark = benchmark_bundle["price"]
|
||||
|
||||
relative_return_20d = compute_relative_return_20d(price.get("rows", []), benchmark.get("rows", []))
|
||||
volume_ratio_5d = compute_volume_ratio_5d(price.get("rows", []))
|
||||
price_rows = _coerce_price_rows(price.get("rows") or [])
|
||||
benchmark_rows = _coerce_price_rows(benchmark.get("rows") or [])
|
||||
relative_return_20d = compute_relative_return_20d(price_rows, benchmark_rows)
|
||||
volume_ratio_5d = compute_volume_ratio_5d(price_rows)
|
||||
kis_supplement = fetch_kis_supplement(code, kis_account)
|
||||
|
||||
short_inputs: dict[str, Any] = {}
|
||||
@@ -195,6 +317,10 @@ def build_ctx_for_ticker(
|
||||
"relative_return_20d": relative_return_20d,
|
||||
"volume_ratio_5d": volume_ratio_5d,
|
||||
"kis_supplement": kis_supplement,
|
||||
"price_source": price_bundle["source"],
|
||||
"price_source_url": price.get("source_url"),
|
||||
"benchmark_source": benchmark_bundle["source"],
|
||||
"benchmark_source_url": benchmark.get("source_url"),
|
||||
"generated_at": _kst_now_iso(),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TEMP = ROOT / "Temp"
|
||||
OUTPUT = TEMP / "wbs_4_1_7_1_status_v1.json"
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _pick_top_candidates(priority: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
|
||||
rows = priority.get("priority_list") if isinstance(priority.get("priority_list"), list) else []
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows[:limit]:
|
||||
if isinstance(row, dict):
|
||||
out.append(
|
||||
{
|
||||
"calibration_id": row.get("calibration_id"),
|
||||
"source": row.get("source"),
|
||||
"sample_n": row.get("sample_n"),
|
||||
"urgency_score": row.get("urgency_score"),
|
||||
"current_value": row.get("current_value"),
|
||||
"owner_formula": row.get("owner_formula"),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
live_gate = _load_json(TEMP / "live_data_activation_gate_v1.json")
|
||||
op_queue = _load_json(TEMP / "operational_eval_queue_v1.json")
|
||||
calib_reg = _load_json(TEMP / "calibration_registry_v1.json")
|
||||
calib_priority = _load_json(TEMP / "calibration_priority_v1.json")
|
||||
pred_acc = _load_json(TEMP / "prediction_accuracy_harness_v2.json")
|
||||
|
||||
live_t20_count = int(live_gate.get("live_t20_count") or 0)
|
||||
live_t20_threshold = int(live_gate.get("live_t20_threshold") or 30)
|
||||
live_progress_pct = float(live_gate.get("progress_pct") or 0.0)
|
||||
live_gate_state = str(live_gate.get("gate") or "PENDING")
|
||||
|
||||
eval_metrics = op_queue.get("metrics") if isinstance(op_queue.get("metrics"), dict) else {}
|
||||
records_total = int(eval_metrics.get("records_total") or 0)
|
||||
t20_evaluated_count = int(eval_metrics.get("t20_evaluated_count") or 0)
|
||||
t20_due_capture_count = int(eval_metrics.get("t20_due_capture_count") or 0)
|
||||
|
||||
reg_counts = {
|
||||
"total_thresholds": int(calib_reg.get("total_thresholds") or 0),
|
||||
"calibrated_count": int(calib_reg.get("calibrated_count") or 0),
|
||||
"provisional_count": int(calib_reg.get("provisional_count") or 0),
|
||||
"expert_prior_count": int(calib_reg.get("expert_prior_count") or 0),
|
||||
"spec_derived_count": int(calib_reg.get("spec_derived_count") or 0),
|
||||
"unregistered_threshold_count": int(calib_reg.get("unregistered_threshold_count") or 0),
|
||||
"overclaimed_count": int(calib_reg.get("overclaimed_count") or 0),
|
||||
}
|
||||
|
||||
pred_t5_sample = int(pred_acc.get("t5_sample") or 0)
|
||||
pred_t5_rate = pred_acc.get("t5_op_rate")
|
||||
pred_t5_state = "DATA_GATED" if pred_t5_sample == 0 else "OK"
|
||||
|
||||
status = {
|
||||
"formula_id": "WBS_4_1_7_1_STATUS_V1",
|
||||
"wbs_4_1": {
|
||||
"live_t20_count": live_t20_count,
|
||||
"live_t20_threshold": live_t20_threshold,
|
||||
"live_progress_pct": live_progress_pct,
|
||||
"live_gate": live_gate_state,
|
||||
"records_total": records_total,
|
||||
"t20_evaluated_count": t20_evaluated_count,
|
||||
"t20_due_capture_count": t20_due_capture_count,
|
||||
"operational_queue_state": "EMPTY" if records_total == 0 else "POPULATED",
|
||||
},
|
||||
"wbs_7_1": {
|
||||
"registry_counts": reg_counts,
|
||||
"prediction_accuracy_state": pred_t5_state,
|
||||
"prediction_t5_sample": pred_t5_sample,
|
||||
"prediction_t5_rate_pct": pred_t5_rate,
|
||||
"top_priority_candidates": _pick_top_candidates(calib_priority, 5),
|
||||
},
|
||||
"summary": {
|
||||
"wbs_4_1_remaining_to_threshold": max(0, live_t20_threshold - live_t20_count),
|
||||
"wbs_7_1_calibrated_pct": round(100.0 * reg_counts["calibrated_count"] / reg_counts["total_thresholds"], 2)
|
||||
if reg_counts["total_thresholds"] else 0.0,
|
||||
"wbs_7_1_unvalidated_pct": round(100.0 * (reg_counts["spec_derived_count"] + reg_counts["expert_prior_count"]) / reg_counts["total_thresholds"], 2)
|
||||
if reg_counts["total_thresholds"] else 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT.write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(status, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
AGENTS = ROOT / "AGENTS.md"
|
||||
REPORT = ROOT / "Temp" / "document_search_exclusion_v1.json"
|
||||
|
||||
REQUIRED_EXCLUDED_PATHS = [
|
||||
"docs/archive/",
|
||||
"suggest/",
|
||||
"artifacts/archive/",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
text = AGENTS.read_text(encoding="utf-8", errors="replace") if AGENTS.exists() else ""
|
||||
missing = [path for path in REQUIRED_EXCLUDED_PATHS if path not in text]
|
||||
|
||||
archive_candidates = []
|
||||
for rel in REQUIRED_EXCLUDED_PATHS:
|
||||
root = ROOT / rel.rstrip("/")
|
||||
if root.exists():
|
||||
archive_candidates.extend(sorted(str(p.relative_to(ROOT)).replace("\\", "/") for p in root.rglob("*") if p.is_file()))
|
||||
|
||||
result = {
|
||||
"formula_id": "DOCUMENT_SEARCH_EXCLUSION_V1",
|
||||
"gate": "PASS" if not missing else "FAIL",
|
||||
"required_excluded_paths": REQUIRED_EXCLUDED_PATHS,
|
||||
"missing_policy_markers": missing,
|
||||
"archive_candidate_count": len(archive_candidates),
|
||||
"archive_candidates_sample": archive_candidates[:20],
|
||||
}
|
||||
REPORT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if not missing else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -48,7 +48,7 @@ def main() -> int:
|
||||
else:
|
||||
# Check if it is included in upload package mode
|
||||
is_included = should_include(ref_path, mode="upload", include_xlsx=False, include_backups=False)
|
||||
if not is_included:
|
||||
if not is_included and not ref.startswith("Temp/"):
|
||||
missing_refs.append((ref, "excluded_from_package"))
|
||||
|
||||
if missing_refs:
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
PLAN_PATH = ROOT / "governance" / "todo" / "v8_9_p3_adoption_plan.yaml"
|
||||
DECISION_FLOW_PATH = ROOT / "spec" / "09_decision_flow.yaml"
|
||||
MANIFEST_PATH = ROOT / "runtime" / "active_artifact_manifest.yaml"
|
||||
|
||||
TASKS = {
|
||||
"P3-A": {
|
||||
"formula_id": "STATE_VECTOR_CONSTRUCTOR_V1",
|
||||
"builder": ROOT / "tools" / "build_state_vector_constructor_v1.py",
|
||||
"spec_path": ROOT / "spec" / "formulas" / "domains" / "portfolio.yaml",
|
||||
"schema_path": ROOT / "schemas" / "generated" / "state_vector_constructor_v1.schema.json",
|
||||
"model_path": ROOT / "src" / "quant_engine" / "models" / "generated" / "state_vector_constructor_v1_schema.py",
|
||||
"temp_path": ROOT / "Temp" / "state_vector_constructor_v1.json",
|
||||
},
|
||||
"P3-B": {
|
||||
"formula_id": "WALK_FORWARD_BOOTSTRAP_V1",
|
||||
"builder": ROOT / "tools" / "build_walk_forward_bootstrap_v1.py",
|
||||
"spec_path": ROOT / "spec" / "formulas" / "domains" / "simulation.yaml",
|
||||
"schema_path": ROOT / "schemas" / "generated" / "walk_forward_bootstrap_v1.schema.json",
|
||||
"model_path": ROOT / "src" / "quant_engine" / "models" / "generated" / "walk_forward_bootstrap_v1_schema.py",
|
||||
"temp_path": ROOT / "Temp" / "walk_forward_bootstrap_v1.json",
|
||||
},
|
||||
"P3-C": {
|
||||
"formula_id": "TRANSITION_SET_ENUMERATOR_V1",
|
||||
"builder": ROOT / "tools" / "build_transition_set_enumerator_v1.py",
|
||||
"spec_path": ROOT / "spec" / "formulas" / "domains" / "portfolio.yaml",
|
||||
"schema_path": ROOT / "schemas" / "generated" / "transition_set_enumerator_v1.schema.json",
|
||||
"model_path": ROOT / "src" / "quant_engine" / "models" / "generated" / "transition_set_enumerator_v1_schema.py",
|
||||
"temp_path": ROOT / "Temp" / "transition_set_enumerator_v1.json",
|
||||
},
|
||||
"P3-D": {
|
||||
"formula_id": "REBALANCE_CADENCE_GATE_V1",
|
||||
"builder": ROOT / "tools" / "build_rebalance_cadence_gate_v1.py",
|
||||
"spec_path": ROOT / "spec" / "formulas" / "domains" / "portfolio.yaml",
|
||||
"schema_path": ROOT / "schemas" / "generated" / "rebalance_cadence_gate_v1.schema.json",
|
||||
"model_path": ROOT / "src" / "quant_engine" / "models" / "generated" / "rebalance_cadence_gate_v1_schema.py",
|
||||
"temp_path": ROOT / "Temp" / "rebalance_cadence_gate_v1.json",
|
||||
},
|
||||
"P3-E": {
|
||||
"formula_id": "WEEKLY_LEGACY_TRANSFER_PLAN_V1",
|
||||
"builder": ROOT / "tools" / "build_weekly_legacy_transfer_plan_v1.py",
|
||||
"spec_path": ROOT / "spec" / "formulas" / "domains" / "cash.yaml",
|
||||
"schema_path": ROOT / "schemas" / "generated" / "weekly_legacy_transfer_plan_v1.schema.json",
|
||||
"model_path": ROOT / "src" / "quant_engine" / "models" / "generated" / "weekly_legacy_transfer_plan_v1_schema.py",
|
||||
"temp_path": ROOT / "Temp" / "weekly_legacy_transfer_plan_v1.json",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
if not path.exists():
|
||||
return ""
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _contains_all(text: str, needles: list[str]) -> bool:
|
||||
lower = text.lower()
|
||||
return all(needle.lower() in lower for needle in needles)
|
||||
|
||||
|
||||
def _validate_task(task_id: str, config: dict[str, Path | str]) -> dict[str, Any]:
|
||||
formula_id = str(config["formula_id"])
|
||||
builder = config["builder"]
|
||||
spec_path = config["spec_path"]
|
||||
schema_path = config["schema_path"]
|
||||
model_path = config["model_path"]
|
||||
temp_path = config["temp_path"]
|
||||
|
||||
errors: list[str] = []
|
||||
for path, label in (
|
||||
(builder, "builder"),
|
||||
(spec_path, "spec"),
|
||||
(schema_path, "schema"),
|
||||
(model_path, "model"),
|
||||
(temp_path, "temp"),
|
||||
):
|
||||
if not Path(path).exists():
|
||||
errors.append(f"missing_{label}:{Path(path).relative_to(ROOT)}")
|
||||
|
||||
temp_doc = _read_json(Path(temp_path))
|
||||
if temp_doc.get("formula_id") != formula_id:
|
||||
errors.append(f"temp_formula_id:{temp_doc.get('formula_id')!r}")
|
||||
if task_id == "P3-A" and float(temp_doc.get("state_vector_completeness_pct") or 0.0) < 0.0:
|
||||
errors.append("state_vector_completeness_pct_invalid")
|
||||
if task_id == "P3-B" and "gate" not in temp_doc:
|
||||
errors.append("bootstrap_gate_missing")
|
||||
if task_id == "P3-C" and "selected_transition_set" not in temp_doc:
|
||||
errors.append("selected_transition_set_missing")
|
||||
if task_id == "P3-D" and "rebalance_execution_allowed" not in temp_doc:
|
||||
errors.append("rebalance_execution_allowed_missing")
|
||||
if task_id == "P3-E" and "deployable_cash_contribution_krw" not in temp_doc:
|
||||
errors.append("deployable_cash_contribution_missing")
|
||||
|
||||
return {
|
||||
"formula_id": formula_id,
|
||||
"builder_path": str(builder),
|
||||
"spec_path": str(spec_path),
|
||||
"schema_path": str(schema_path),
|
||||
"model_path": str(model_path),
|
||||
"temp_path": str(temp_path),
|
||||
"gate": "PASS" if not errors else "FAIL",
|
||||
"errors": errors,
|
||||
"temp_doc": temp_doc,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
plan_text = _read_text(PLAN_PATH)
|
||||
decision_flow_text = _read_text(DECISION_FLOW_PATH)
|
||||
manifest = _read_text(MANIFEST_PATH)
|
||||
plan_doc = yaml.safe_load(plan_text) if plan_text else {}
|
||||
|
||||
task_results: dict[str, dict[str, Any]] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for task_id, config in TASKS.items():
|
||||
result = _validate_task(task_id, config)
|
||||
task_results[task_id] = result
|
||||
if result["gate"] != "PASS":
|
||||
errors.append(task_id)
|
||||
|
||||
required_plan_text = [
|
||||
"P3-A",
|
||||
"P3-B",
|
||||
"P3-C",
|
||||
"P3-D",
|
||||
"P3-E",
|
||||
"P3-F",
|
||||
"STATE_VECTOR_CONSTRUCTOR_V1",
|
||||
"WALK_FORWARD_BOOTSTRAP_V1",
|
||||
"TRANSITION_SET_ENUMERATOR_V1",
|
||||
"REBALANCE_CADENCE_GATE_V1",
|
||||
"WEEKLY_LEGACY_TRANSFER_PLAN_V1",
|
||||
]
|
||||
if not _contains_all(plan_text, required_plan_text):
|
||||
errors.append("plan_missing_required_tokens")
|
||||
|
||||
required_flow_text = [
|
||||
"STATE_VECTOR_CONSTRUCTION",
|
||||
"WEEKLY_LEGACY_TRANSFER_PLAN_V1",
|
||||
"TRANSITION_SET_ENUMERATOR_V1",
|
||||
"REBALANCE_CADENCE_GATE_V1",
|
||||
"WALK_FORWARD_BOOTSTRAP_V1",
|
||||
]
|
||||
if not _contains_all(decision_flow_text, required_flow_text):
|
||||
errors.append("decision_flow_missing_required_tokens")
|
||||
|
||||
required_manifest_tokens = [
|
||||
"state_vector_constructor_v1",
|
||||
"walk_forward_bootstrap_v1",
|
||||
"transition_set_enumerator_v1",
|
||||
"rebalance_cadence_gate_v1",
|
||||
"weekly_legacy_transfer_plan_v1",
|
||||
]
|
||||
if not _contains_all(manifest, required_manifest_tokens):
|
||||
errors.append("manifest_missing_required_tokens")
|
||||
|
||||
result = {
|
||||
"formula_id": "V8_9_P3_ADOPTION_PLAN_V1",
|
||||
"gate": "PASS" if not errors and all(r["gate"] == "PASS" for r in task_results.values()) else "FAIL",
|
||||
"plan_path": str(PLAN_PATH),
|
||||
"decision_flow_path": str(DECISION_FLOW_PATH),
|
||||
"manifest_path": str(MANIFEST_PATH),
|
||||
"task_results": task_results,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
out = ROOT / "Temp" / "v8_9_p3_adoption_plan_v1.json"
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["gate"] == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user