"""PORTFOLIO_ALPHA_CONFIDENCE_PER_TICKER_V1 — 종목별 알파 신뢰도 산출기. data_feed의 시그널들을 결합하여 종목별 PAC(Portfolio Alpha Confidence) 점수를 산출. 점수 구성 (-100 ~ +100): entry_freshness 35점: MA20 이격도 기반 (낮을수록 신선한 진입) breakout_quality 25점: Breakout_Score 기반 flow_acceleration 20점: Val_Surge_Pct (거래량 급등 비율) fundamental_signal 10점: fundamental_multifactor_v3 점수 기반 rs_slope 10점: RS_Line_20D_Slope 기반 라벨: BULLISH ≥ +30 NEUTRAL ≥ -10 BEARISH < -10 출력: Temp/portfolio_alpha_confidence_per_ticker_v1.json """ from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_FUND = ROOT / "Temp" / "fundamental_multifactor_v3.json" DEFAULT_OUT = ROOT / "Temp" / "portfolio_alpha_confidence_per_ticker_v1.json" def _load(path: Path) -> dict[str, Any]: try: d = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return d if isinstance(d, dict) else {} def _rows(v: Any) -> list[dict[str, Any]]: if isinstance(v, list): return [r for r in v if isinstance(r, dict)] if isinstance(v, str): try: return _rows(json.loads(v)) except Exception: return [] return [] def _f(v: Any, default: float = 0.0) -> float: try: return float(v) except Exception: return default def _entry_freshness_score(disparity_pct: float, rsi14: float) -> float: """진입 신선도: 이격도 낮고 RSI 적정 구간이면 좋음.""" # disparity가 0~5% 이내면 신선함: +35 # 10% 이상이면 오버슈팅: -35 d_score = 0.0 if disparity_pct <= 5.0: d_score = 35.0 elif disparity_pct <= 10.0: d_score = 20.0 elif disparity_pct <= 20.0: d_score = 0.0 else: d_score = -20.0 # RSI: 30~60이면 좋음 r_score = 0.0 if 30 <= rsi14 <= 60: r_score = 15.0 elif 20 <= rsi14 < 30 or 60 < rsi14 <= 70: r_score = 5.0 elif rsi14 > 70: r_score = -10.0 return d_score + r_score def _breakout_quality_score(breakout_score: float) -> float: """브레이크아웃 품질 (0~100 스케일의 breakout_score → -25~+25).""" if breakout_score >= 60: return 25.0 elif breakout_score >= 40: return 10.0 elif breakout_score >= 20: return 0.0 else: return -15.0 def _flow_accel_score(val_surge_pct: float) -> float: """거래량 급등 비율 → 모멘텀 점수.""" if val_surge_pct >= 3.0: return 20.0 elif val_surge_pct >= 1.5: return 10.0 elif val_surge_pct >= 0.5: return 5.0 else: return 0.0 def _fund_signal_score(grade: str) -> float: """펀더멘털 등급 → 알파 기여도.""" grade_map = {"A": 10.0, "B": 7.0, "C": 3.0, "D": -3.0, "F": -8.0, "ETF": 0.0} return grade_map.get(grade, 0.0) def _rs_slope_score(rs_slope: float) -> float: """RS Line 20일 기울기 → 상대강도 기여도.""" if rs_slope > 0.5: return 10.0 elif rs_slope > 0: return 5.0 elif rs_slope > -0.5: return 0.0 else: return -8.0 def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--json", default=str(DEFAULT_JSON)) ap.add_argument("--fund", default=str(DEFAULT_FUND)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() jp = Path(args.json) fp = Path(args.fund) op = Path(args.out) if not jp.is_absolute(): jp = ROOT / jp if not fp.is_absolute(): fp = ROOT / fp if not op.is_absolute(): op = ROOT / op payload = _load(jp) data = payload.get("data") if isinstance(payload.get("data"), dict) else {} df_list = _rows(data.get("data_feed")) # 펀더멘털 등급 조회 fund_rows = _rows(_load(fp).get("rows")) fund_map = {str(r.get("ticker") or ""): r for r in fund_rows} rows = [] vals: list[float] = [] for r in df_list: t = str(r.get("Ticker") or r.get("ticker") or "") name = r.get("Name") or r.get("name") or "" disparity = _f(r.get("Disparity")) rsi14 = _f(r.get("RSI14"), 50.0) breakout = _f(r.get("Breakout_Score")) val_surge = _f(r.get("Val_Surge_Pct")) rs_slope = _f(r.get("RS_Line_20D_Slope")) fund_info = fund_map.get(t, {}) grade = str(fund_info.get("grade") or "F") s_entry = _entry_freshness_score(abs(disparity), rsi14) s_breakout = _breakout_quality_score(breakout) s_flow = _flow_accel_score(val_surge) s_fund = _fund_signal_score(grade) s_rs = _rs_slope_score(rs_slope) pac = round(s_entry + s_breakout + s_flow + s_fund + s_rs, 2) pac = max(-100.0, min(100.0, pac)) vals.append(pac) label = "BULLISH" if pac >= 30 else ("NEUTRAL" if pac >= -10 else "BEARISH") rows.append({ "ticker": t, "name": name, "pac_score": pac, "pac_label": label, "breakdown": { "entry_freshness": round(s_entry, 2), "breakout_quality": round(s_breakout, 2), "flow_accel": round(s_flow, 2), "fundamental": round(s_fund, 2), "rs_slope": round(s_rs, 2), }, "fundamental_grade": grade, "formula_id": "PORTFOLIO_ALPHA_CONFIDENCE_PER_TICKER_V1", }) mean = sum(vals) / len(vals) if vals else 0.0 var = sum((v - mean) ** 2 for v in vals) / len(vals) if vals else 0.0 std = var ** 0.5 label_diversity = len({r["pac_label"] for r in rows}) label_summary: dict[str, int] = {} for r in rows: lbl = r["pac_label"] label_summary[lbl] = label_summary.get(lbl, 0) + 1 gate = "PASS" if (std >= 5.0 and label_diversity >= 2) else ( "CAUTION" if rows else "FAIL" ) out = { "formula_id": "PORTFOLIO_ALPHA_CONFIDENCE_PER_TICKER_V1", "gate": gate, "rows": rows, "row_count": len(rows), "stddev": round(std, 2), "label_diversity": label_diversity, "label_summary": label_summary, } op.parent.mkdir(parents=True, exist_ok=True) op.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps({ "formula_id": out["formula_id"], "gate": gate, "stddev": out["stddev"], "label_summary": label_summary, }, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())