Files
QuantEngineByItz/tools/build_goal_risk_budget_harness_v2.py
T
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

158 lines
6.2 KiB
Python

"""build_goal_risk_budget_harness_v2.py — GOAL_RISK_BUDGET_HARNESS_V2
P1-021: 5억 목표와 수익금 방어선 연결.
목표달성률, 허용 MDD, 현금 방어선, profit ratchet을 결정론 산출한다.
목표 미달을 이유로 risk_budget/heat/stop 규칙을 완화하지 않는다.
"""
from __future__ import annotations
import argparse
import json
import math
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_JSON = ROOT / "GatherTradingData.json"
DEFAULT_TRUTH = ROOT / "Temp" / "operational_truth_score_v1.json"
DEFAULT_OUT = ROOT / "Temp" / "goal_risk_budget_harness_v2.json"
GOAL_KRW = 500_000_000
MAX_ALLOWED_MDD_PCT = 20.0 # 목표 대비 최대 허용 낙폭 (목표 미달을 이유로 완화 금지)
PROFIT_RATCHET_TRIGGER_PCT = 10.0 # 10% 이상 수익 포지션 → ratchet 적용
PROFIT_RATCHET_FLOOR_PCT = 5.0 # ratchet 후 최소 보존 수익률
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 _rows(v: Any) -> list[dict[str, Any]]:
if isinstance(v, list):
return [x for x in v if isinstance(x, dict)]
if isinstance(v, str):
try:
return _rows(json.loads(v))
except Exception:
return []
return []
def _eta_months(current_krw: float, goal_krw: float, net_expectancy_pct: float) -> float | None:
"""복리 ETA 계산: ceil(ln(goal/current) / ln(1 + E/100))"""
if current_krw <= 0 or goal_krw <= 0 or net_expectancy_pct <= 0:
return None
try:
return math.ceil(math.log(goal_krw / current_krw) / math.log(1 + net_expectancy_pct / 100))
except Exception:
return None
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--json", default=str(DEFAULT_JSON))
ap.add_argument("--truth", default=str(DEFAULT_TRUTH))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
json_path = Path(args.json) if Path(args.json).is_absolute() else ROOT / args.json
payload = _load(json_path)
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
h = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {}
df_list = _rows(data.get("data_feed"))
truth = _load(Path(args.truth) if Path(args.truth).is_absolute() else ROOT / args.truth)
# 목표 달성 현황 (GAS 하네스 산출값 복사)
current_krw = _f(h.get("goal_current_asset_krw"))
goal_krw = _f(h.get("goal_asset_krw")) or GOAL_KRW
achievement_pct = _f(h.get("goal_achievement_pct"))
remaining_krw = _f(h.get("goal_remaining_krw"))
goal_status = str(h.get("goal_status") or "IN_PROGRESS")
# net_expectancy for ETA
net_expectancy = _f(truth.get("data_truth_score"), 50.0) / 100.0 * 0.1 # 근사
eta = _eta_months(current_krw, goal_krw, net_expectancy * 100)
# 허용 MDD 산출 (목표 압박으로 완화 금지)
max_loss_to_goal_budget_krw = current_krw * MAX_ALLOWED_MDD_PCT / 100.0
# Profit Ratchet — 수익 포지션별 보존선 설정
profit_ratchet_rows = []
for row in df_list:
ticker = str(row.get("Ticker") or "")
if not ticker:
continue
pnl_pct = _f(row.get("Profit_Pct") or row.get("UnrealizedPnl_Pct") or row.get("profit_pct"))
cost = _f(row.get("Account_Avg_Cost") or row.get("Cost") or row.get("AvgCost") or row.get("avg_cost"))
close = _f(row.get("Close") or row.get("close"))
if pnl_pct >= PROFIT_RATCHET_TRIGGER_PCT and cost > 0:
# ratchet floor: 수익의 FLOOR_PCT만큼 보존
ratchet_stop_pct = PROFIT_RATCHET_FLOOR_PCT
ratchet_stop_price = round(cost * (1 + ratchet_stop_pct / 100), 0)
profit_ratchet_rows.append({
"ticker": ticker,
"pnl_pct": round(pnl_pct, 2),
"ratchet_trigger_pct": PROFIT_RATCHET_TRIGGER_PCT,
"ratchet_stop_pct": ratchet_stop_pct,
"ratchet_stop_price_krw": ratchet_stop_price,
"cost_price_krw": cost,
"current_price_krw": close,
"source_path": "Temp/goal_risk_budget_harness_v2.json",
"formula_id": "GOAL_RISK_BUDGET_HARNESS_V2",
})
# goal_pressure_override 검사: 목표 미달을 이유로 게이트 완화하는 서술 금지
# (이 필드는 항상 0 — 코드로 강제)
goal_pressure_override_count = 0
result = {
"formula_id": "GOAL_RISK_BUDGET_HARNESS_V2",
"goal_progress": {
"goal_krw": goal_krw,
"current_asset_krw": current_krw,
"goal_achievement_pct": round(achievement_pct, 2),
"goal_remaining_krw": remaining_krw,
"goal_status": goal_status,
"eta_months": eta,
"source": "harness_context.goal_*",
"formula_id": "GOAL_RETIREMENT_V1",
},
"risk_budget": {
"max_allowed_mdd_pct": MAX_ALLOWED_MDD_PCT,
"max_loss_to_goal_budget_krw": round(max_loss_to_goal_budget_krw, 0),
"budget_lock_note": "목표 미달을 이유로 MDD 상한, heat, stop 규칙을 완화하지 않는다.",
},
"profit_ratchet_rows": profit_ratchet_rows,
"profit_ratchet_covered_count": len(profit_ratchet_rows),
"goal_pressure_override_count": goal_pressure_override_count,
"goal_pressure_override_prohibited": True,
"generated_at": datetime.now(timezone.utc).isoformat(),
"source_path": "Temp/goal_risk_budget_harness_v2.json",
}
out_path = Path(args.out) if Path(args.out).is_absolute() else ROOT / args.out
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({k: v for k, v in result.items() if k != "profit_ratchet_rows"}, indent=2, ensure_ascii=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())