Files
QuantEngineByItz/tools/build_final_judgment_gate_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

570 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())