Files
QuantEngineByItz/tools/build_operational_outcome_lock_v1.py
kjh2064 ee3e799de1 feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경:
- 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>
2026-06-13 13:20:14 +09:00

146 lines
5.3 KiB
Python

from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_HISTORY = ROOT / "Temp" / "proposal_evaluation_history.json"
DEFAULT_OUTCOME = ROOT / "Temp" / "outcome_quality_score_v1.json"
DEFAULT_EXEC = ROOT / "Temp" / "execution_quality_harness_v1.json"
DEFAULT_PERF = ROOT / "Temp" / "perf_recovery_harness_v1.json"
DEFAULT_OUT = ROOT / "Temp" / "operational_outcome_lock_v1.json"
def _load(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
try:
obj = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
return obj if isinstance(obj, dict) else {}
def _f(v: Any, default: float = 0.0) -> float:
try:
return float(v)
except Exception:
return default
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--history", default=str(DEFAULT_HISTORY))
ap.add_argument("--outcome", default=str(DEFAULT_OUTCOME))
ap.add_argument("--execution", default=str(DEFAULT_EXEC))
ap.add_argument("--perf", default=str(DEFAULT_PERF))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
hp = Path(args.history)
op = Path(args.outcome)
ep = Path(args.execution)
pp = Path(args.perf)
out = Path(args.out)
if not hp.is_absolute():
hp = ROOT / hp
if not op.is_absolute():
op = ROOT / op
if not ep.is_absolute():
ep = ROOT / ep
if not pp.is_absolute():
pp = ROOT / pp
if not out.is_absolute():
out = ROOT / out
history = _load(hp)
outcome = _load(op)
execution = _load(ep)
perf = _load(pp)
records = history.get("records") if isinstance(history.get("records"), list) else []
t5_oper = [
r for r in records if isinstance(r, dict)
and r.get("t5_evaluation_status") == "EVALUATED_T5"
and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL"
]
t20_oper = [
r for r in records if isinstance(r, dict)
and r.get("t20_evaluation_status") == "EVALUATED_T20"
and str(r.get("validation_status") or "").upper() != "REPLAY_BACKFILL"
]
t20_match = len([r for r in t20_oper if str(r.get("t20_outcome") or "") == "MATCHED"])
t20_rate = round((t20_match / len(t20_oper)) * 100.0, 2) if t20_oper else 0.0
oq_score = _f(outcome.get("score"))
eq_metrics = (execution.get("metrics") or {}).get("operational_t20") if isinstance((execution.get("metrics") or {}).get("operational_t20"), dict) else {}
expectancy = _f(eq_metrics.get("expectancy_pct"))
win_rate = _f(eq_metrics.get("win_rate_pct"))
late_precision = _f((perf.get("metrics") or {}).get("late_chase_block_precision"), 0.0)
value_damage = _f((perf.get("metrics") or {}).get("rebound_sell_value_damage"), 0.0)
# [Work 29] threshold 현실화
# execution_quality_harness_v1.json 미존재 시 EXPECTANCY/WIN_RATE는 "데이터 없음"으로 처리
# (실거래 기록 없음 = 알 수 없음, 아닌 0%)
_exec_data_available = ep.exists() and bool(eq_metrics)
reasons: list[str] = []
if len(t20_oper) < 30:
reasons.append("OPERATIONAL_T20_SAMPLE_LT_30")
if t20_rate < 60.0:
reasons.append("OPERATIONAL_T20_PASS_LT_60")
if oq_score < 60.0:
reasons.append("OUTCOME_QUALITY_LT_60")
# EXPECTANCY/WIN_RATE: 실거래 T+20 표본이 충분할 때만 체크 (samples>0)
_exec_samples = int(_f(eq_metrics.get("samples"), 0.0))
if _exec_data_available and _exec_samples >= 10:
if expectancy <= 0.1:
reasons.append("EXPECTANCY_LE_0_1")
if win_rate < 45.0:
reasons.append("WIN_RATE_LT_45")
# VALUE_DAMAGE: 10% 초과는 실행 차단
if value_damage > 10.0:
reasons.append("VALUE_DAMAGE_GT_10")
if late_precision < 80.0 and late_precision > 0.0:
reasons.append("LATE_CHASE_PRECISION_LOW")
unlock_state = "PERFORMANCE_READY" if not reasons else "WATCH_PENDING_SAMPLE"
if any(x in reasons for x in ("OUTCOME_QUALITY_LT_60", "EXPECTANCY_LE_0_1", "WIN_RATE_LT_45", "VALUE_DAMAGE_GT_10")):
unlock_state = "NOT_PERFORMANCE_READY"
result = {
"formula_id": "OPERATIONAL_OUTCOME_LOCK_V1",
"unlock_state": unlock_state,
"reasons": reasons,
"metrics": {
"operational_t5_count": len(t5_oper),
"operational_t20_count": len(t20_oper),
"operational_t20_pass_rate": t20_rate,
"outcome_quality_score": oq_score,
"execution_expectancy_pct": expectancy,
"execution_win_rate_pct": win_rate,
"late_chase_block_precision": late_precision,
"sell_after_rebound_damage_pct": value_damage,
},
"targets": {
"operational_t20_count_min": 30,
"operational_t20_pass_rate_min": 60.0,
"outcome_quality_score_min": 60.0,
"execution_expectancy_pct_min": 0.1,
"execution_win_rate_pct_min": 45.0,
"sell_after_rebound_damage_pct_max": 10.0,
},
}
out.parent.mkdir(parents=True, exist_ok=True)
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())