ee3e799de1
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
353 lines
17 KiB
Python
353 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# ── 셀-레벨 커버리지: yaml expected_outputs → operational_report 셀 매핑 ──────
|
|
# 각 formula의 expected_outputs 필드가 operational_report의 표 셀에 채워졌는지 측정.
|
|
# _CELL_COVERAGE_STUBS: 채워진 것처럼 보이지만 실제 데이터 없는 일률 stub 값들
|
|
_CELL_COVERAGE_STUBS = frozenset({
|
|
"", "-", "n/a", "N/A", "데이터 누락", "DATA_MISSING", "중립", "NEUTRAL",
|
|
"LOSING", "정상", "NORMAL", "MISSING", "WATCH_PENDING_SAMPLE",
|
|
})
|
|
|
|
|
|
def load_yaml(path: Path) -> dict[str, Any]:
|
|
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
def load_json_safe(path: Path) -> dict[str, Any]:
|
|
if not path.exists():
|
|
return {}
|
|
try:
|
|
v = json.loads(path.read_text(encoding="utf-8"))
|
|
return v if isinstance(v, dict) else {}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def formula_registry() -> dict[str, Any]:
|
|
"""formula_id → formula_dict (expected_outputs 포함)."""
|
|
registry: dict[str, Any] = {}
|
|
for p in (ROOT / "spec" / "13_formula_registry.yaml", ROOT / "spec" / "13b_harness_formulas.yaml"):
|
|
y = load_yaml(p)
|
|
fm = ((y.get("formula_registry") or {}).get("formulas")) or {}
|
|
for k, v in fm.items():
|
|
if isinstance(v, dict):
|
|
registry[str(k)] = v
|
|
return registry
|
|
|
|
|
|
def formula_ids() -> list[str]:
|
|
return sorted(formula_registry().keys())
|
|
|
|
|
|
def read_texts(paths: list[Path]) -> str:
|
|
chunks: list[str] = []
|
|
for p in paths:
|
|
if p.exists():
|
|
chunks.append(p.read_text(encoding="utf-8", errors="ignore"))
|
|
return "\n".join(chunks)
|
|
|
|
|
|
def _extract_table_cells(markdown: str) -> set[str]:
|
|
"""GFM 표에서 셀 값 목록을 추출 (헤더 + 데이터 행)."""
|
|
cells: set[str] = set()
|
|
for line in markdown.split("\n"):
|
|
if "|" not in line:
|
|
continue
|
|
parts = [p.strip() for p in line.split("|")]
|
|
for p in parts:
|
|
if p and p != "---" and not re.match(r"^-+$", p):
|
|
cells.add(p)
|
|
return cells
|
|
|
|
|
|
def _is_stub(value: str) -> bool:
|
|
return value.strip() in _CELL_COVERAGE_STUBS or value.strip().startswith("-")
|
|
|
|
|
|
def measure_cell_coverage(
|
|
formula_reg: dict[str, Any],
|
|
report_json: dict[str, Any],
|
|
harness_ctx: dict[str, Any],
|
|
temp_outputs: dict[str, dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""yaml expected_outputs → 4경로 커버리지.
|
|
|
|
출력 필드가 채워진 것으로 인정하는 4가지 경로:
|
|
1. GAS harness_context에 output.field 키가 non-null 존재
|
|
2. Phase-1 Temp JSON 파일에 expected_output 필드가 non-stub 존재
|
|
3. operational_report 섹션 텍스트에 expected_output 이름이 column header로 존재
|
|
4. operational_report 표 셀에 non-stub 값으로 필드명=값 패턴 존재
|
|
"""
|
|
# Collect all report text (markdown)
|
|
all_section_text = ""
|
|
for sec in report_json.get("sections") or []:
|
|
all_section_text += " " + (sec.get("markdown") or "")
|
|
|
|
# Flatten all Temp output values for quick lookup
|
|
temp_flat: dict[str, Any] = {}
|
|
for _fname, tdata in temp_outputs.items():
|
|
if isinstance(tdata, dict):
|
|
# Flatten top-level scalars and row-level fields
|
|
for k, v in tdata.items():
|
|
if k not in ("rows", "steps", "selected_combo"):
|
|
temp_flat[k] = v
|
|
# Also include fields from first row of any rows list
|
|
for listkey in ("rows", "steps", "selected_combo"):
|
|
lst = tdata.get(listkey)
|
|
if isinstance(lst, list) and lst and isinstance(lst[0], dict):
|
|
for k, v in lst[0].items():
|
|
temp_flat.setdefault(k, v)
|
|
|
|
required_outputs: list[dict[str, Any]] = []
|
|
for fid, fdef in formula_reg.items():
|
|
if not isinstance(fdef, dict):
|
|
continue
|
|
# orphan reconcile 공식은 GAS/보고서 셀 검사 제외 (Python harness 전용)
|
|
if str(fdef.get("version", "")).endswith("_ORPHAN_RECONCILE"):
|
|
continue
|
|
exp = fdef.get("expected_outputs")
|
|
if not isinstance(exp, list):
|
|
continue
|
|
out_field = (fdef.get("output") or {}).get("field") if isinstance(fdef.get("output"), dict) else None
|
|
|
|
# Path 1: GAS harness_context
|
|
ctx_present = bool(out_field and harness_ctx.get(out_field) is not None)
|
|
|
|
for o in exp:
|
|
field_name = str(o).strip() if isinstance(o, str) else str(o).strip()
|
|
|
|
# Path 2: Temp JSON outputs (Phase-1 Python tools)
|
|
temp_val = temp_flat.get(field_name)
|
|
temp_filled = temp_val is not None and str(temp_val).strip() not in _CELL_COVERAGE_STUBS
|
|
|
|
# Path 3: Column header in report
|
|
in_report_header = bool(field_name and field_name in all_section_text)
|
|
|
|
# Path 4: Row-level cell value (non-stub)
|
|
non_stub_value = False
|
|
pat = re.search(
|
|
rf"\b{re.escape(field_name)}\b[^|\n]*\|([^|\n]+)", all_section_text
|
|
)
|
|
if pat:
|
|
val_candidate = pat.group(1).strip()
|
|
non_stub_value = not _is_stub(val_candidate)
|
|
|
|
filled = ctx_present or temp_filled or (in_report_header and non_stub_value)
|
|
|
|
required_outputs.append({
|
|
"formula_id": fid,
|
|
"output_field": field_name,
|
|
"ctx_present": ctx_present,
|
|
"temp_filled": temp_filled,
|
|
"in_report_header": in_report_header,
|
|
"non_stub_value": non_stub_value,
|
|
"filled": filled,
|
|
})
|
|
|
|
total = len(required_outputs)
|
|
filled_count = sum(1 for r in required_outputs if r["filled"])
|
|
cell_coverage_pct = round(filled_count / total * 100, 2) if total > 0 else 0.0
|
|
unfilled = [r for r in required_outputs if not r["filled"]]
|
|
|
|
return {
|
|
"total_required_outputs": total,
|
|
"filled_outputs": filled_count,
|
|
"cell_coverage_pct": cell_coverage_pct,
|
|
"unfilled_outputs": unfilled,
|
|
"cell_gate": "PASS" if cell_coverage_pct >= 95.0 else ("CAUTION" if cell_coverage_pct >= 75.0 else "FAIL"),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Measure YAML formula coverage in GS and PS governance layers.")
|
|
parser.add_argument("--strict-100", action="store_true")
|
|
parser.add_argument("--output-json", default=str(ROOT / "Temp" / "yaml_gs_ps_coverage.json"))
|
|
parser.add_argument("--report-json", default=str(ROOT / "Temp" / "operational_report.json"))
|
|
args = parser.parse_args()
|
|
|
|
reg = formula_registry()
|
|
ids = sorted(reg.keys())
|
|
_GS_CORE = [ROOT / "gas_data_feed.gs", ROOT / "gas_harness_rows.gs", ROOT / "gas_lib.gs", ROOT / "gas_data_collect.gs", ROOT / "gas_report.gs"]
|
|
_GAS_ADAPTER_DIR = ROOT / "src" / "gas_adapter_parts"
|
|
_gs_adapter_files = sorted(_GAS_ADAPTER_DIR.glob("*.gs")) if _GAS_ADAPTER_DIR.is_dir() else []
|
|
_GS_ALPHA_WATCH = [ROOT / "gas_apex_alpha_watch.gs", ROOT / "gas_apex_runtime_core.gs"]
|
|
gs_text = read_texts(_GS_CORE + _gs_adapter_files + _GS_ALPHA_WATCH)
|
|
ps_text = read_texts([ROOT / "tools" / "run_engine_harness_gate.ps1", ROOT / "tools" / "run_yolo_full_cycle.ps1"])
|
|
gate_py_text = read_texts([ROOT / "tools" / "validate_engine_harness_gate.py"])
|
|
|
|
gs_hit = [i for i in ids if i in gs_text]
|
|
gs_miss = [i for i in ids if i not in gs_text]
|
|
|
|
# PS는 공식 직접 계산 계층이 아니라 실행 강제 계층.
|
|
ps_required_hooks = [
|
|
("run_engine_harness_gate.ps1", ROOT / "tools" / "run_engine_harness_gate.ps1"),
|
|
("run_yolo_full_cycle.ps1", ROOT / "tools" / "run_yolo_full_cycle.ps1"),
|
|
("validate_engine_harness_gate.py", ROOT / "tools" / "validate_engine_harness_gate.py"),
|
|
]
|
|
ps_hook_hit = [name for name, path in ps_required_hooks if path.exists()]
|
|
ps_hook_miss = [name for name, path in ps_required_hooks if not path.exists()]
|
|
|
|
total = len(ids) if ids else 1
|
|
gs_pct = round(len(gs_hit) / total * 100, 2)
|
|
ps_pct = round(len(ps_hook_hit) / len(ps_required_hooks) * 100, 2)
|
|
|
|
# ── 셀-레벨 커버리지 측정 ──────────────────────────────────────────────────
|
|
report_json_path = Path(args.report_json)
|
|
if not report_json_path.is_absolute():
|
|
report_json_path = ROOT / report_json_path
|
|
report_json = load_json_safe(report_json_path)
|
|
# harness context from GatherTradingData.json
|
|
gtd = load_json_safe(ROOT / "GatherTradingData.json")
|
|
hctx = (gtd.get("data") or {}).get("_harness_context") or {}
|
|
# Phase-1/2/3 Temp outputs (Python tools)
|
|
_TEMP = ROOT / "Temp"
|
|
temp_outputs = {
|
|
# Phase-1
|
|
"ejce_view_renderer_v1": load_json_safe(_TEMP / "ejce_view_renderer_v1.json"),
|
|
"smart_cash_recovery_v3": load_json_safe(_TEMP / "smart_cash_recovery_v3.json"),
|
|
"ratchet_trailing_v1": load_json_safe(_TEMP / "ratchet_trailing_general_v1.json"),
|
|
"value_preservation_v1": load_json_safe(_TEMP / "value_preservation_scorer_v1.json"),
|
|
"routing_execution_log_v1": load_json_safe(_TEMP / "routing_execution_log_v1.json"),
|
|
"blank_cell_audit_v1": load_json_safe(_TEMP / "blank_cell_audit_v1.json"),
|
|
"formula_registry_sync_v1": load_json_safe(_TEMP / "formula_registry_sync_v1.json"),
|
|
# Phase-2
|
|
"fundamental_raw_v1": load_json_safe(_TEMP / "fundamental_raw_v1.json"),
|
|
"fundamental_multifactor_v3": load_json_safe(_TEMP / "fundamental_multifactor_v3.json"),
|
|
"horizon_classification_v1": load_json_safe(_TEMP / "horizon_classification_v1.json"),
|
|
# Phase-2B
|
|
"earnings_quality_signal_v1": load_json_safe(_TEMP / "earnings_quality_signal_v1.json"),
|
|
"growth_rate_signal_v1": load_json_safe(_TEMP / "growth_rate_signal_v1.json"),
|
|
"cashflow_quality_signal_v1": load_json_safe(_TEMP / "cashflow_quality_signal_v1.json"),
|
|
"market_share_signal_v2": load_json_safe(_TEMP / "market_share_signal_v2.json"),
|
|
# Phase-3
|
|
"smart_money_flow_signal_v2": load_json_safe(_TEMP / "smart_money_flow_signal_v2.json"),
|
|
"liquidity_flow_signal_v1": load_json_safe(_TEMP / "liquidity_flow_signal_v1.json"),
|
|
"capital_style_allocation_v1": load_json_safe(_TEMP / "capital_style_allocation_v1.json"),
|
|
"portfolio_alpha_confidence_per_ticker_v1": load_json_safe(_TEMP / "portfolio_alpha_confidence_per_ticker_v1.json"),
|
|
# [Advanced Harness Architecture]
|
|
"dynamic_value_preservation_sell_v6": load_json_safe(_TEMP / "dynamic_value_preservation_sell_v6.json"),
|
|
"predictive_alpha_engine_v2": load_json_safe(_TEMP / "predictive_alpha_engine_v2.json"),
|
|
"capital_style_time_stop_v1": load_json_safe(_TEMP / "capital_style_time_stop_v1.json"),
|
|
"execution_integrity_gate_v1": load_json_safe(_TEMP / "execution_integrity_gate_v1.json"),
|
|
# Phase-6 Python-tool-only
|
|
"final_judgment_gate_v1": load_json_safe(_TEMP / "final_judgment_gate_v1.json"),
|
|
"verdict_consistency_lock_v1": load_json_safe(_TEMP / "verdict_consistency_lock_v1.json"),
|
|
"data_quality_reconciliation_v1": load_json_safe(_TEMP / "data_quality_reconciliation_v1.json"),
|
|
}
|
|
cell_cov = measure_cell_coverage(reg, report_json, hctx, temp_outputs)
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
# Python-tool-only formulas: not in GAS (implemented as Python tools)
|
|
_PYTHON_TOOL_FORMULAS = {
|
|
# Phase-1
|
|
"BLANK_CELL_AUDIT_V1", "VALUE_PRESERVATION_SCORER_V1",
|
|
"SMART_CASH_RECOVERY_V3", "RATCHET_TRAILING_GENERAL_V1",
|
|
"EJCE_VIEW_RENDERER_V1", "ROUTING_EXECUTION_LOG_TABLE_V1",
|
|
# Phase-2
|
|
"FUNDAMENTAL_RAW_INGEST_V1", "FUNDAMENTAL_MULTIFACTOR_V3",
|
|
"HORIZON_CLASSIFICATION_V1",
|
|
# Phase-2B
|
|
"EARNINGS_QUALITY_SIGNAL_V1", "GROWTH_RATE_SIGNAL_V1",
|
|
"CASHFLOW_QUALITY_SIGNAL_V1",
|
|
# Phase-3
|
|
"SMART_MONEY_FLOW_SIGNAL_V2", "LIQUIDITY_FLOW_SIGNAL_V1",
|
|
"PORTFOLIO_ALPHA_CONFIDENCE_PER_TICKER_V1",
|
|
# Phase-3 Market Share V2
|
|
"MARKET_SHARE_SIGNAL_V2",
|
|
# [Advanced Harness Architecture]
|
|
"DYNAMIC_VALUE_PRESERVATION_SELL_V6", "PREDICTIVE_ALPHA_DIALECTIC_ENGINE_V2",
|
|
"CAPITAL_STYLE_TIME_STOP_V1", "EXECUTION_INTEGRITY_GATE_V1",
|
|
# Phase-4~5 Python-tool-only 공식 (GAS 구현 없음, Python tools로 구현)
|
|
"TRADE_QUALITY_FROM_T5_V1", "PREDICTION_ACCURACY_HARNESS_V2",
|
|
"MACRO_EVENT_TICKER_IMPACT_V1", "SELL_WATERFALL_ENGINE_V2",
|
|
"LLM_NARRATIVE_TEMPLATE_LOCK_V1", "EJCE_DIVERGENCE_AUDIT_V1",
|
|
"PREDICTIVE_ALPHA_REPORT_LOCK_V2",
|
|
# Phase-6 Python-tool-only 공식 (판단 결정론 계층)
|
|
"FINAL_JUDGMENT_GATE_V1", "VERDICT_CONSISTENCY_LOCK_V1",
|
|
"INVESTMENT_QUALITY_HEADLINE_V1",
|
|
# Phase-7 단일 진실원천 + 교차섹션 정합성 (Python-tool-only, GAS 구현 불필요)
|
|
"CANONICAL_METRICS_V1", "CROSS_SECTION_CONSISTENCY_V1",
|
|
# Work 7 + Work 3 분석 도구
|
|
"ALPHA_FEEDBACK_LOOP_V2", "ALPHA_LEAD_THRESHOLD_OPTIMIZER_V1",
|
|
# Registry sync: formulas implemented outside GAS coverage path
|
|
"VELOCITY_V1", "PROFIT_LOCK_STAGE_V1", "ANTI_LATE_ENTRY_GATE_V2",
|
|
"DYNAMIC_HEAT_GATE_V1", "POSITION_SIZE_REGIME_SCALE_V1",
|
|
"REGIME_CASH_UPLIFT_V1", "DRAWDOWN_GUARD_V1", "POSITION_COUNT_LIMIT_V1",
|
|
"CASH_FLOOR_V1", "SEMICONDUCTOR_CLUSTER_GATE_V1",
|
|
"SINGLE_POSITION_WEIGHT_CAP_V1", "REGIME_TRIM_GUIDANCE_V1",
|
|
"HEAT_CONCENTRATION_ALERT_V1", "SECTOR_CONCENTRATION_LIMIT_V1",
|
|
"PORTFOLIO_DRAWDOWN_GATE_V1", "K2_STAGED_REBOUND_SELL_V1",
|
|
"STOP_BREACH_ALERT_V1", "SECTOR_ROTATION_MOMENTUM_V1",
|
|
"ANTI_WHIPSAW_GATE_V1", "BREAKEVEN_RATCHET_V1",
|
|
"MARKET_WEIGHT_AWARE_CLUSTER_GATE_V1", "LEADER_POSITION_WEIGHT_CAP_V1",
|
|
"CAPITAL_STYLE_ALLOCATION_V1",
|
|
# ENGINE_AUDIT_V1 — Python-tool-only 감사 게이트 (GAS 런타임 비개입)
|
|
"IMPUTED_DATA_EXPOSURE_GATE_V1",
|
|
"SCORES_HARNESS_V1",
|
|
"STRATEGY_ROUTING_AUDIT_V1",
|
|
"SELL_ENGINE_AUDIT_V1",
|
|
"YAML_TO_CODE_COVERAGE_V1",
|
|
"REALIZED_PERFORMANCE_V1",
|
|
"BACKTEST_HARNESS_V1",
|
|
# NF1~NF5: GAS execution_order 제외 Python-harness 전용 보조 공식 (python_harness_supplements 등록)
|
|
"REGIME_CONDITIONAL_MACRO_FACTOR_V1", # NF1 — tools/build_predictive_alpha_dialectic_engine_v2.py
|
|
"REBOUND_CAPTURE_THESIS_FACTOR_V1", # NF2 — tools/build_predictive_alpha_dialectic_engine_v2.py
|
|
"ENTRY_TIMING_DECILE_FACTOR_V1", # NF3 — tools/build_late_chase_attribution_v1.py
|
|
"SELL_SLIPPAGE_BUDGET_FACTOR_V1", # NF4 — tools/build_value_preservation_scorer_v1.py
|
|
"PROFIT_GIVEBACK_RATCHET_FACTOR_V1", # NF5 — tools/build_ratchet_trailing_general_v1.py
|
|
# Phase-execution Python-tool-only (tools/build_execution_method_ladder_v1.py, runtime=PYTHON)
|
|
"EXECUTION_METHOD_LADDER_V1",
|
|
}
|
|
# V9 orphan reconcile — _ORPHAN_RECONCILE 버전 태그 공식은 GAS 요구사항 면제
|
|
ids_to_skip = {fid for fid, fdef in reg.items() if isinstance(fdef, dict) and str(fdef.get("version", "")).endswith("_ORPHAN_RECONCILE")}
|
|
_PYTHON_TOOL_FORMULAS = _PYTHON_TOOL_FORMULAS | ids_to_skip
|
|
block_gs_miss = [f for f in gs_miss if f not in _PYTHON_TOOL_FORMULAS]
|
|
summary = {
|
|
"formula_total": len(ids),
|
|
"gs_covered": len(gs_hit),
|
|
"gs_missing": gs_miss,
|
|
"gs_coverage_pct": gs_pct,
|
|
"gs_blocking_missing": block_gs_miss,
|
|
"ps_required_hooks": [name for name, _ in ps_required_hooks],
|
|
"ps_hook_covered": len(ps_hook_hit),
|
|
"ps_hook_missing": ps_hook_miss,
|
|
"ps_coverage_pct": ps_pct,
|
|
"cell_coverage": cell_cov,
|
|
"status": "OK" if (gs_pct >= 100.0 and ps_pct == 100.0 and cell_cov["cell_gate"] != "FAIL") else "FAIL",
|
|
}
|
|
|
|
out = Path(args.output_json)
|
|
if not out.is_absolute():
|
|
out = ROOT / out
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
print(
|
|
f"YAML_GS_PS_COVERAGE: gs={gs_pct:.2f}% "
|
|
f"ps={ps_pct:.2f}% total={len(ids)} "
|
|
f"cell_coverage={cell_cov['cell_coverage_pct']:.2f}% [{cell_cov['cell_gate']}]"
|
|
)
|
|
if summary["status"] == "OK":
|
|
print("YAML_GS_PS_COVERAGE_OK")
|
|
return 0
|
|
print("YAML_GS_PS_COVERAGE_FAIL")
|
|
if args.strict_100:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|