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>
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
"""FINAL_JUDGMENT_GATE_V1
|
||||
판단 결정론 계층 — 모든 게이트·신호 JSON + _harness_context를 읽어
|
||||
종목별 단일 action_verdict를 결정론으로 산출한다.
|
||||
|
||||
action_verdict ∈ {BUY_PILOT, HOLD, TRIM, SELL, WATCH, BLOCKED}
|
||||
|
||||
매도 판단 우선순위:
|
||||
1. SELL : stop_breach=BREACH OR sell_waterfall stage_label ∈ {EMERGENCY_FULL, DISTRIBUTION_EXIT}
|
||||
2. TRIM : sell_waterfall stage_label ∈ {URGENT_TRIM, TRIM_ONLY}
|
||||
3. TP_TRIM: tp_trigger=TRIGGERED (미래 확장 예약)
|
||||
|
||||
매수 판단 AND-11 조건 (전부 PASS여야 BUY_PILOT):
|
||||
J01. anti_late_entry(alpha_lead buy_permission_state ≠ BLOCKED)
|
||||
J02. distribution(anti_distribution_state ≠ BLOCK_BUY)
|
||||
J03. breakout(Breakout_Gate ≠ WAIT OR lead_entry_state ≠ BLOCKED_LATE_CHASE)
|
||||
J04. smart_money_liquidity(gate_status ≠ BLOCK_BUY)
|
||||
J05. liquidity(execution_mode ≠ FROZEN)
|
||||
J06. macro(primary_gate ∉ {AVOID_NEW_BUY, HEDGE})
|
||||
J07. fundamental(grade ∈ {A,B,C} — ETF 제외)
|
||||
J08. heat_gate(heat_gate_status ≠ BLOCK_NEW_BUY)
|
||||
J09. cash_floor(cash_floor_status == PASS)
|
||||
J10. position_count(position_count_gate ≠ POSITION_COUNT_BLOCK)
|
||||
J11. single_position_weight(single_position_weight_gate ≠ OVERWEIGHT_TRIM)
|
||||
|
||||
harness_key 부재 → DATA_MISSING (silent PASS 금지)
|
||||
|
||||
effective_confidence = round(raw_confidence × (0.4 + 0.6 × invest_quality_score/100), 1)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _ensure_utf8_stdio() -> None:
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stderr = open(sys.stderr.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_JSON = ROOT / "GatherTradingData.json"
|
||||
DEFAULT_SM_GATE = ROOT / "Temp" / "smart_money_liquidity_gate_v1.json"
|
||||
DEFAULT_LIQUIDITY = ROOT / "Temp" / "liquidity_flow_signal_v1.json"
|
||||
DEFAULT_MACRO = ROOT / "Temp" / "macro_event_ticker_impact_v1.json"
|
||||
DEFAULT_FUNDAMENTAL = ROOT / "Temp" / "fundamental_multifactor_v3.json"
|
||||
DEFAULT_HORIZON = ROOT / "Temp" / "horizon_classification_v1.json"
|
||||
DEFAULT_SELL_WATERFALL = ROOT / "Temp" / "sell_waterfall_engine_v2.json"
|
||||
DEFAULT_DQ_RECONCILE = ROOT / "Temp" / "data_quality_reconciliation_v1.json"
|
||||
DEFAULT_OUT = ROOT / "Temp" / "final_judgment_gate_v1.json"
|
||||
|
||||
# 매수 차단 집합
|
||||
MACRO_BLOCK_NEW_BUY = {"AVOID_NEW_BUY", "HEDGE"}
|
||||
FUND_PASS_GRADES = {"A", "B", "C"}
|
||||
HEAT_BLOCK_VALUES = {"BLOCK_NEW_BUY"}
|
||||
CASH_PASS_VALUES = {"PASS"}
|
||||
POSITION_COUNT_BLOCK_VALUES = {"POSITION_COUNT_BLOCK"}
|
||||
SINGLE_WEIGHT_BLOCK_VALUES = {"OVERWEIGHT_TRIM", "OVERWEIGHT_BLOCK"}
|
||||
|
||||
# 매도 waterfall stage 레이블 → verdict 매핑
|
||||
SELL_STAGE_LABELS = {"EMERGENCY_FULL", "DISTRIBUTION_EXIT"}
|
||||
TRIM_STAGE_LABELS = {"URGENT_TRIM", "TRIM_ONLY"}
|
||||
|
||||
# BUY AND-11 게이트 키
|
||||
AND_GATE_KEYS = [
|
||||
"J01_anti_late_entry",
|
||||
"J02_distribution",
|
||||
"J03_breakout",
|
||||
"J04_smart_money_liquidity",
|
||||
"J05_liquidity",
|
||||
"J06_macro",
|
||||
"J07_fundamental",
|
||||
"J08_heat_gate",
|
||||
"J09_cash_floor",
|
||||
"J10_position_count",
|
||||
"J11_single_position_weight",
|
||||
]
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any] | list:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
obj = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
return obj
|
||||
|
||||
|
||||
def _as_dict(obj: Any) -> dict[str, Any]:
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_harness_root(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
h_apex = payload.get("hApex")
|
||||
data_apex = ((payload.get("data") or {}).get("_harness_context")) if isinstance(payload.get("data"), dict) else None
|
||||
if isinstance(h_apex, dict) and isinstance(data_apex, dict):
|
||||
merged = dict(data_apex)
|
||||
merged.update(h_apex)
|
||||
return merged
|
||||
if isinstance(h_apex, dict):
|
||||
return h_apex
|
||||
if isinstance(data_apex, dict):
|
||||
return data_apex
|
||||
return payload
|
||||
|
||||
|
||||
def _extract_data_feed(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
data_node = payload.get("data") or {}
|
||||
if isinstance(data_node, dict):
|
||||
feed = data_node.get("data_feed")
|
||||
if isinstance(feed, list):
|
||||
return feed
|
||||
return []
|
||||
|
||||
|
||||
def _rows_by_ticker(obj: Any) -> dict[str, dict[str, Any]]:
|
||||
"""JSON 산출물(list 또는 {rows:[]} dict)을 ticker→row dict로 변환."""
|
||||
rows: list[dict[str, Any]] = []
|
||||
if isinstance(obj, list):
|
||||
rows = [r for r in obj if isinstance(r, dict)]
|
||||
elif isinstance(obj, dict):
|
||||
for key in ("rows", "tickers", "data"):
|
||||
candidate = obj.get(key)
|
||||
if isinstance(candidate, list):
|
||||
rows = [r for r in candidate if isinstance(r, dict)]
|
||||
break
|
||||
return {str(r.get("ticker") or ""): r for r in rows if r.get("ticker")}
|
||||
|
||||
|
||||
def _parse_hctx_list(hctx: dict[str, Any], key: str) -> dict[str, dict[str, Any]]:
|
||||
val = hctx.get(key)
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
val = json.loads(val)
|
||||
except Exception:
|
||||
return {}
|
||||
return _rows_by_ticker(val or {})
|
||||
|
||||
|
||||
def _as_float(v: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# BUY AND-11 게이트 평가 함수들
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _gate_result(gate_key: str, status: str, value: Any, detail: str = "") -> dict[str, Any]:
|
||||
return {"gate": gate_key, "status": status, "value": value, "detail": detail}
|
||||
|
||||
|
||||
def _eval_j01_anti_late_entry(
|
||||
alpha_row: dict[str, Any] | None,
|
||||
df_row: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""J01: anti_late_entry — alpha_lead buy_permission_state ≠ BLOCKED."""
|
||||
if alpha_row is None:
|
||||
return _gate_result("J01_anti_late_entry", "DATA_MISSING", None, "alpha_lead_json row not found")
|
||||
bps = str(alpha_row.get("buy_permission_state") or "")
|
||||
passed = bps != "BLOCKED"
|
||||
return _gate_result(
|
||||
"J01_anti_late_entry",
|
||||
"PASS" if passed else "BLOCK",
|
||||
bps,
|
||||
str(alpha_row.get("lead_entry_state") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _eval_j02_distribution(dist_row: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""J02: distribution — anti_distribution_state ≠ BLOCK_BUY."""
|
||||
if dist_row is None:
|
||||
return _gate_result("J02_distribution", "DATA_MISSING", None, "distribution_risk_json row not found")
|
||||
ads = str(dist_row.get("anti_distribution_state") or "")
|
||||
passed = ads != "BLOCK_BUY"
|
||||
return _gate_result(
|
||||
"J02_distribution",
|
||||
"PASS" if passed else "BLOCK",
|
||||
ads,
|
||||
f"score={dist_row.get('distribution_risk_score')}",
|
||||
)
|
||||
|
||||
|
||||
def _eval_j03_breakout(
|
||||
alpha_row: dict[str, Any] | None,
|
||||
df_row: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""J03: breakout — Breakout_Gate ≠ WAIT OR alpha_lead confirmed."""
|
||||
breakout_gate = str(df_row.get("Breakout_Gate") or "MISSING")
|
||||
if alpha_row is not None:
|
||||
les = str(alpha_row.get("lead_entry_state") or "")
|
||||
confirmed = breakout_gate not in {"WAIT", "MISSING"} or les not in {"BLOCKED_LATE_CHASE", "WATCH"}
|
||||
else:
|
||||
confirmed = breakout_gate not in {"WAIT", "MISSING"}
|
||||
return _gate_result(
|
||||
"J03_breakout",
|
||||
"PASS" if confirmed else "BLOCK",
|
||||
breakout_gate,
|
||||
f"Breakout_Score={df_row.get('Breakout_Score')}",
|
||||
)
|
||||
|
||||
|
||||
def _eval_j04_smart_money(sm_row: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""J04: smart_money_liquidity gate_status ≠ BLOCK_BUY."""
|
||||
if sm_row is None:
|
||||
return _gate_result("J04_smart_money_liquidity", "DATA_MISSING", None, "smart_money_liquidity_gate row not found")
|
||||
gs = str(sm_row.get("gate_status") or "")
|
||||
passed = gs != "BLOCK_BUY"
|
||||
return _gate_result("J04_smart_money_liquidity", "PASS" if passed else "BLOCK", gs)
|
||||
|
||||
|
||||
def _eval_j05_liquidity(lq_row: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""J05: liquidity execution_mode ≠ FROZEN."""
|
||||
if lq_row is None:
|
||||
return _gate_result("J05_liquidity", "DATA_MISSING", None, "liquidity_flow_signal row not found")
|
||||
em = str(lq_row.get("execution_mode") or "")
|
||||
passed = em != "FROZEN"
|
||||
return _gate_result("J05_liquidity", "PASS" if passed else "BLOCK", em)
|
||||
|
||||
|
||||
def _eval_j06_macro(macro_row: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""J06: macro primary_gate ∉ {AVOID_NEW_BUY, HEDGE}."""
|
||||
if macro_row is None:
|
||||
return _gate_result("J06_macro", "DATA_MISSING", None, "macro_event_ticker_impact row not found")
|
||||
pg = str(macro_row.get("primary_gate") or "")
|
||||
passed = pg not in MACRO_BLOCK_NEW_BUY
|
||||
return _gate_result("J06_macro", "PASS" if passed else "BLOCK", pg)
|
||||
|
||||
|
||||
def _eval_j07_fundamental(fund_row: dict[str, Any] | None, is_etf: bool) -> dict[str, Any]:
|
||||
"""J07: fundamental grade ∈ {A,B,C} — ETF 제외."""
|
||||
if is_etf:
|
||||
return _gate_result("J07_fundamental", "EXEMPT", "ETF", "ETF는 펀더멘털 게이트 면제")
|
||||
if fund_row is None:
|
||||
return _gate_result("J07_fundamental", "DATA_MISSING", None, "fundamental_multifactor row not found")
|
||||
grade = str(fund_row.get("grade") or "")
|
||||
passed = grade in FUND_PASS_GRADES
|
||||
return _gate_result("J07_fundamental", "PASS" if passed else "BLOCK", grade)
|
||||
|
||||
|
||||
def _eval_j08_heat(hctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""J08: heat_gate_status ≠ BLOCK_NEW_BUY (포트폴리오 레벨)."""
|
||||
status = str(hctx.get("heat_gate_status") or "")
|
||||
if not status:
|
||||
return _gate_result("J08_heat_gate", "DATA_MISSING", None, "heat_gate_status not found")
|
||||
passed = status not in HEAT_BLOCK_VALUES
|
||||
return _gate_result("J08_heat_gate", "PASS" if passed else "BLOCK", status)
|
||||
|
||||
|
||||
def _eval_j09_cash_floor(hctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""J09: cash_floor_status == PASS."""
|
||||
status = str(hctx.get("cash_floor_status") or "")
|
||||
if not status:
|
||||
return _gate_result("J09_cash_floor", "DATA_MISSING", None, "cash_floor_status not found")
|
||||
passed = status in CASH_PASS_VALUES
|
||||
return _gate_result("J09_cash_floor", "PASS" if passed else "BLOCK", status)
|
||||
|
||||
|
||||
def _eval_j10_position_count(hctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""J10: position_count_gate ≠ POSITION_COUNT_BLOCK."""
|
||||
status = str(hctx.get("position_count_gate") or "")
|
||||
if not status:
|
||||
return _gate_result("J10_position_count", "DATA_MISSING", None, "position_count_gate not found")
|
||||
passed = status not in POSITION_COUNT_BLOCK_VALUES
|
||||
return _gate_result("J10_position_count", "PASS" if passed else "BLOCK", status)
|
||||
|
||||
|
||||
def _eval_j11_single_weight(hctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""J11: single_position_weight_gate ≠ OVERWEIGHT (포트폴리오 레벨)."""
|
||||
status = str(hctx.get("single_position_weight_gate") or "")
|
||||
if not status:
|
||||
return _gate_result("J11_single_position_weight", "DATA_MISSING", None, "single_position_weight_gate not found")
|
||||
passed = status not in SINGLE_WEIGHT_BLOCK_VALUES
|
||||
return _gate_result("J11_single_position_weight", "PASS" if passed else "BLOCK", status)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 매도 신호 평가
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _eval_stop_breach(sb_row: dict[str, Any] | None) -> str:
|
||||
"""BREACH → SELL, APPROACHING → WARN, else SAFE."""
|
||||
if sb_row is None:
|
||||
return "SAFE"
|
||||
return str(sb_row.get("stop_breach_gate") or "SAFE")
|
||||
|
||||
|
||||
def _eval_tp_trigger(tp_row: dict[str, Any] | None) -> str:
|
||||
"""TP 트리거 상태."""
|
||||
if tp_row is None:
|
||||
return "NONE"
|
||||
return str(tp_row.get("tp_trigger_gate") or "NONE")
|
||||
|
||||
|
||||
def _eval_sell_waterfall(sw_row: dict[str, Any] | None) -> tuple[int, str]:
|
||||
"""(stage, stage_label) — 미존재 시 (0, NONE)."""
|
||||
if sw_row is None:
|
||||
return 0, "NONE"
|
||||
stage = int(sw_row.get("stage") or 0)
|
||||
label = str(sw_row.get("stage_label") or "NONE")
|
||||
return stage, label
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 종목별 판단 집약
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _compute_raw_confidence(and_trace: list[dict[str, Any]]) -> float:
|
||||
"""AND-11 결과로부터 원시 신뢰도 산출 (PASS/EXEMPT=1.0, DATA_MISSING=0.5, BLOCK=0.0)."""
|
||||
if not and_trace:
|
||||
return 0.0
|
||||
weights = {"PASS": 1.0, "EXEMPT": 1.0, "DATA_MISSING": 0.5, "BLOCK": 0.0}
|
||||
total = sum(weights.get(g["status"], 0.0) for g in and_trace)
|
||||
return round(total / len(and_trace) * 100.0, 1)
|
||||
|
||||
|
||||
def _compute_effective_confidence(raw: float, invest_quality_score: float) -> float:
|
||||
"""effective_confidence = raw × (0.4 + 0.6 × iq/100)."""
|
||||
cap_factor = 0.4 + 0.6 * (invest_quality_score / 100.0)
|
||||
return round(raw * cap_factor, 1)
|
||||
|
||||
|
||||
def _evaluate_ticker(
|
||||
df_row: dict[str, Any],
|
||||
hctx: dict[str, Any],
|
||||
alpha_by_ticker: dict[str, dict],
|
||||
dist_by_ticker: dict[str, dict],
|
||||
sm_by_ticker: dict[str, dict],
|
||||
lq_by_ticker: dict[str, dict],
|
||||
macro_by_ticker: dict[str, dict],
|
||||
fund_by_ticker: dict[str, dict],
|
||||
horizon_by_ticker: dict[str, dict],
|
||||
sb_by_ticker: dict[str, dict],
|
||||
tp_by_ticker: dict[str, dict],
|
||||
sw_by_ticker: dict[str, dict],
|
||||
invest_quality_score: float,
|
||||
) -> dict[str, Any]:
|
||||
ticker = str(df_row.get("Ticker") or "UNKNOWN")
|
||||
name = str(df_row.get("Name") or "")
|
||||
is_etf = str(df_row.get("SS001_Grade") or "") == "ETF" or ticker in {"0117V0", "494670", "471990", "091160"}
|
||||
|
||||
# fund row로 is_etf 재확인
|
||||
fund_row = fund_by_ticker.get(ticker)
|
||||
if fund_row is not None:
|
||||
is_etf = bool(fund_row.get("is_etf") or False)
|
||||
|
||||
# AND-11 게이트 평가
|
||||
and_trace: list[dict[str, Any]] = [
|
||||
_eval_j01_anti_late_entry(alpha_by_ticker.get(ticker), df_row),
|
||||
_eval_j02_distribution(dist_by_ticker.get(ticker)),
|
||||
_eval_j03_breakout(alpha_by_ticker.get(ticker), df_row),
|
||||
_eval_j04_smart_money(sm_by_ticker.get(ticker)),
|
||||
_eval_j05_liquidity(lq_by_ticker.get(ticker)),
|
||||
_eval_j06_macro(macro_by_ticker.get(ticker)),
|
||||
_eval_j07_fundamental(fund_row, is_etf),
|
||||
_eval_j08_heat(hctx),
|
||||
_eval_j09_cash_floor(hctx),
|
||||
_eval_j10_position_count(hctx),
|
||||
_eval_j11_single_weight(hctx),
|
||||
]
|
||||
|
||||
blocking_gates = [g["gate"] for g in and_trace if g["status"] == "BLOCK"]
|
||||
data_missing_gates = [g["gate"] for g in and_trace if g["status"] == "DATA_MISSING"]
|
||||
|
||||
# 매도 신호 평가
|
||||
stop_breach_status = _eval_stop_breach(sb_by_ticker.get(ticker))
|
||||
tp_status = _eval_tp_trigger(tp_by_ticker.get(ticker))
|
||||
sw_stage, sw_label = _eval_sell_waterfall(sw_by_ticker.get(ticker))
|
||||
|
||||
# 호라이즌
|
||||
horiz_row = horizon_by_ticker.get(ticker)
|
||||
horizon = str((horiz_row or {}).get("horizon") or "UNKNOWN")
|
||||
|
||||
# 신뢰도 산출
|
||||
raw_confidence = _compute_raw_confidence(and_trace)
|
||||
effective_confidence = _compute_effective_confidence(raw_confidence, invest_quality_score)
|
||||
|
||||
# ── Verdict 결정 (우선순위 엄격 적용) ──────────────────────────────────
|
||||
verdict_reason: list[str] = []
|
||||
|
||||
if stop_breach_status == "BREACH":
|
||||
action_verdict = "SELL"
|
||||
verdict_reason.append(f"stop_breach=BREACH")
|
||||
elif sw_label in SELL_STAGE_LABELS:
|
||||
action_verdict = "SELL"
|
||||
verdict_reason.append(f"sell_waterfall={sw_label}(stage={sw_stage})")
|
||||
elif sw_label in TRIM_STAGE_LABELS and sw_stage > 0:
|
||||
action_verdict = "TRIM"
|
||||
verdict_reason.append(f"sell_waterfall={sw_label}(stage={sw_stage})")
|
||||
elif tp_status == "TRIGGERED":
|
||||
action_verdict = "TRIM"
|
||||
verdict_reason.append(f"tp_trigger=TRIGGERED")
|
||||
elif not blocking_gates and not data_missing_gates:
|
||||
action_verdict = "BUY_PILOT"
|
||||
verdict_reason.append("all_AND11_pass")
|
||||
elif blocking_gates:
|
||||
action_verdict = "BLOCKED"
|
||||
verdict_reason.extend([f"blocked_by:{g}" for g in blocking_gates[:3]])
|
||||
elif data_missing_gates:
|
||||
action_verdict = "WATCH"
|
||||
verdict_reason.append(f"data_missing:{len(data_missing_gates)}gates")
|
||||
else:
|
||||
action_verdict = "HOLD"
|
||||
verdict_reason.append("no_active_signals")
|
||||
|
||||
# stop_approaching → WATCH 강등(HOLD만 해당, SELL/TRIM 아닌 경우)
|
||||
if stop_breach_status == "APPROACHING" and action_verdict in {"HOLD", "BUY_PILOT"}:
|
||||
action_verdict = "WATCH"
|
||||
verdict_reason.append("stop_approaching")
|
||||
|
||||
return {
|
||||
"ticker": ticker,
|
||||
"name": name,
|
||||
"action_verdict": action_verdict,
|
||||
"verdict_reason": verdict_reason,
|
||||
"raw_confidence": raw_confidence,
|
||||
"effective_confidence": effective_confidence,
|
||||
"invest_quality_cap_factor": round(0.4 + 0.6 * (invest_quality_score / 100.0), 3),
|
||||
"horizon": horizon,
|
||||
"is_etf": is_etf,
|
||||
"and_trace": and_trace,
|
||||
"blocking_gates": blocking_gates,
|
||||
"data_missing_gates": data_missing_gates,
|
||||
"sell_signals": {
|
||||
"stop_breach_gate": stop_breach_status,
|
||||
"tp_trigger_gate": tp_status,
|
||||
"sell_waterfall_stage": sw_stage,
|
||||
"sell_waterfall_label": sw_label,
|
||||
},
|
||||
"formula_id": "FINAL_JUDGMENT_GATE_V1",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_ensure_utf8_stdio()
|
||||
ap = argparse.ArgumentParser(description="FINAL_JUDGMENT_GATE_V1 — 판단 결정론 계층")
|
||||
ap.add_argument("--json", default=str(DEFAULT_JSON))
|
||||
ap.add_argument("--sm-gate", default=str(DEFAULT_SM_GATE))
|
||||
ap.add_argument("--liquidity", default=str(DEFAULT_LIQUIDITY))
|
||||
ap.add_argument("--macro", default=str(DEFAULT_MACRO))
|
||||
ap.add_argument("--fundamental", default=str(DEFAULT_FUNDAMENTAL))
|
||||
ap.add_argument("--horizon", default=str(DEFAULT_HORIZON))
|
||||
ap.add_argument("--sell-waterfall", default=str(DEFAULT_SELL_WATERFALL))
|
||||
ap.add_argument("--dq-reconcile", default=str(DEFAULT_DQ_RECONCILE))
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
def _rp(s: str) -> Path:
|
||||
p = Path(s)
|
||||
return p if p.is_absolute() else ROOT / p
|
||||
|
||||
json_path = _rp(args.json)
|
||||
out_path = _rp(args.out)
|
||||
|
||||
# ─── 데이터 로드 ─────────────────────────────────────────────────────
|
||||
main_data = _as_dict(_load_json(json_path))
|
||||
hctx = _extract_harness_root(main_data)
|
||||
feed = _extract_data_feed(main_data)
|
||||
if not feed:
|
||||
print("ERROR: data_feed 비어 있음", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
sm_gate = _as_dict(_load_json(_rp(args.sm_gate)))
|
||||
lq_data = _load_json(_rp(args.liquidity))
|
||||
macro_data = _as_dict(_load_json(_rp(args.macro)))
|
||||
fund_data = _as_dict(_load_json(_rp(args.fundamental)))
|
||||
horizon_data = _as_dict(_load_json(_rp(args.horizon)))
|
||||
sw_data = _as_dict(_load_json(_rp(args.sell_waterfall)))
|
||||
dq_data = _as_dict(_load_json(_rp(args.dq_reconcile)))
|
||||
|
||||
invest_quality_source_score = float(dq_data.get("investment_quality_score") or 0.0)
|
||||
invest_quality_cap_basis_score = float(dq_data.get("confidence_cap_basis_score") or invest_quality_source_score or 0.0)
|
||||
invest_quality_score = invest_quality_cap_basis_score or invest_quality_source_score
|
||||
|
||||
# harness_context에서 per-ticker JSON 추출
|
||||
alpha_by_ticker = _parse_hctx_list(hctx, "alpha_lead_json")
|
||||
dist_by_ticker = _parse_hctx_list(hctx, "distribution_risk_json")
|
||||
sb_by_ticker = _parse_hctx_list(hctx, "stop_breach_alert_json")
|
||||
tp_by_ticker = _parse_hctx_list(hctx, "tp_trigger_alert_json")
|
||||
|
||||
# 외부 JSON 파일에서 per-ticker 추출
|
||||
sm_by_ticker = _rows_by_ticker(sm_gate)
|
||||
lq_by_ticker = _rows_by_ticker(lq_data)
|
||||
macro_by_ticker = _rows_by_ticker(macro_data)
|
||||
fund_by_ticker = _rows_by_ticker(fund_data)
|
||||
horizon_by_ticker = _rows_by_ticker(horizon_data)
|
||||
sw_by_ticker = _rows_by_ticker(sw_data)
|
||||
|
||||
# ─── 평가 실행 ────────────────────────────────────────────────────────
|
||||
rows: list[dict[str, Any]] = []
|
||||
verdict_counts: dict[str, int] = {}
|
||||
silent_pass_violations = 0 # 반드시 0이어야 함
|
||||
|
||||
for df_row in feed:
|
||||
result = _evaluate_ticker(
|
||||
df_row, hctx,
|
||||
alpha_by_ticker, dist_by_ticker, sm_by_ticker,
|
||||
lq_by_ticker, macro_by_ticker, fund_by_ticker,
|
||||
horizon_by_ticker, sb_by_ticker, tp_by_ticker,
|
||||
sw_by_ticker, invest_quality_score,
|
||||
)
|
||||
rows.append(result)
|
||||
v = result["action_verdict"]
|
||||
verdict_counts[v] = verdict_counts.get(v, 0) + 1
|
||||
|
||||
# silent PASS 감시: DATA_MISSING이 있는데 BUY_PILOT이면 위반
|
||||
if result["data_missing_gates"] and v == "BUY_PILOT":
|
||||
silent_pass_violations += 1
|
||||
|
||||
# 뒷박 후보 검증 (velocity≥3% OR distribution≥2.0인데 BUY_PILOT이면 위반)
|
||||
late_chase_buy_violations = []
|
||||
for r in rows:
|
||||
if r["action_verdict"] != "BUY_PILOT":
|
||||
continue
|
||||
ticker = r["ticker"]
|
||||
df_row = next((x for x in feed if x.get("Ticker") == ticker), {})
|
||||
ret5d = abs(float(df_row.get("Ret5D") or 0.0))
|
||||
dist_r = dist_by_ticker.get(ticker) or {}
|
||||
dist_score = int(dist_r.get("distribution_risk_score") or 0)
|
||||
if ret5d >= 3.0 or dist_score >= 60:
|
||||
late_chase_buy_violations.append({"ticker": ticker, "ret5d": ret5d, "dist_score": dist_score})
|
||||
|
||||
coverage_pct = round(100.0 * len(rows) / max(1, len(feed)), 2)
|
||||
gate_ok = "PASS" if (silent_pass_violations == 0 and not late_chase_buy_violations) else "FAIL"
|
||||
|
||||
out = {
|
||||
"formula_id": "FINAL_JUDGMENT_GATE_V1",
|
||||
"gate": gate_ok,
|
||||
"coverage_pct": coverage_pct,
|
||||
"ticker_count": len(rows),
|
||||
"invest_quality_score": invest_quality_score,
|
||||
"invest_quality_source_score": invest_quality_source_score,
|
||||
"invest_quality_cap_basis_score": invest_quality_cap_basis_score,
|
||||
"invest_quality_cap_factor": round(0.4 + 0.6 * (invest_quality_score / 100.0), 3),
|
||||
"verdict_counts": verdict_counts,
|
||||
"silent_pass_violations": silent_pass_violations,
|
||||
"late_chase_buy_violations": late_chase_buy_violations,
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print("FINAL_JUDGMENT_GATE_V1")
|
||||
print(f" tickers: {len(rows)}/{len(feed)} (coverage={coverage_pct}%)")
|
||||
print(f" invest_quality_score: {invest_quality_score} (source={invest_quality_source_score}) → cap_factor={round(0.4+0.6*invest_quality_score/100,3)}")
|
||||
print(f" verdict_counts: {verdict_counts}")
|
||||
print(f" silent_pass_violations: {silent_pass_violations} (반드시 0)")
|
||||
print(f" late_chase_buy_violations: {len(late_chase_buy_violations)} (반드시 0)")
|
||||
print(f" gate: {gate_ok}")
|
||||
print()
|
||||
print(f" {'TICKER':<10} {'VERDICT':<12} {'RAW_CONF':>8} {'EFF_CONF':>8} BLOCKING_GATES")
|
||||
for r in rows:
|
||||
blocking = ",".join(r['blocking_gates'][:3]) if r['blocking_gates'] else "NONE"
|
||||
print(f" {r['ticker']:<10} {r['action_verdict']:<12} {r['raw_confidence']:>8.1f} {r['effective_confidence']:>8.1f} {blocking}")
|
||||
return 0 if gate_ok == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user