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:
2026-06-13 13:20:14 +09:00
commit ee3e799de1
1474 changed files with 176087 additions and 0 deletions
+569
View File
@@ -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())