"""build_smart_money_liquidity_composite_v3.py — SMART_MONEY_LIQUIDITY_COMPOSITE_V3 P0-013: 수급과 유동성의 실전 합성 V3. 모든 보유 종목에 slippage_bps를 채우고, 수급 방향성을 1D/3D/5D/20D로 분리 저장하며, 수급 약세+유동성 양호 종목의 cash_raise 후보 점수를 상향한다. 수급 약세 종목 신규 BUY를 차단한다. """ from __future__ import annotations import argparse import json 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_OUT = ROOT / "Temp" / "smart_money_liquidity_composite_v3.json" DEFAULT_LIQUIDITY_SIGNAL = ROOT / "Temp" / "liquidity_flow_signal_v1.json" # 슬리피지 임계값 (억 단위 기준) SLIPPAGE_MARKET_THRESHOLD = 50_000 # 500억 이상: MARKET SLIPPAGE_TWAP_THRESHOLD = 5_000 # 50억 이상: TWAP # < 50억: LIMIT 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 _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 _f(v: Any, default: float = 0.0) -> float: try: return float(v) except Exception: return default def _tick_size(price: float) -> float: if price < 1_000: return 1.0 if price < 5_000: return 5.0 if price < 10_000: return 10.0 if price < 50_000: return 50.0 if price < 100_000: return 100.0 if price < 500_000: return 500.0 return 1_000.0 def _slippage_bps(price: float, atv_m: float) -> float: """예상 슬리피지 (basis points). 유동성 기반 조정.""" if price <= 0: return 0.0 tick = _tick_size(price) base_slip = (tick / price) * 10_000 # 1tick 슬리피지 # 유동성 낮을수록 슬리피지 증가 (ATVm = 백만원 단위) if atv_m >= SLIPPAGE_MARKET_THRESHOLD: multiplier = 1.0 elif atv_m >= SLIPPAGE_TWAP_THRESHOLD: multiplier = 2.0 else: multiplier = 4.0 return round(base_slip * multiplier, 1) def _exec_mode(atv_m: float) -> str: if atv_m >= SLIPPAGE_MARKET_THRESHOLD: return "MARKET" if atv_m >= SLIPPAGE_TWAP_THRESHOLD: return "TWAP" return "LIMIT" def _liquidity_state(atv_m: float) -> tuple[str, str, float]: if atv_m >= 200_000: return "DEEP", "MARKET_OK", 100.0 if atv_m >= 50_000: return "NORMAL", "LIMIT_NEAR_BID", 70.0 if atv_m > 5_000: return "THIN", "TWAP_SPLIT", 40.0 return "FROZEN", "HOLD", 0.0 def _flow_direction(flow_1d: float | None, flow_3d: float | None, flow_5d: float | None, flow_20d: float | None) -> str: """수급 방향성 판정.""" values = [v for v in [flow_1d, flow_3d, flow_5d, flow_20d] if v is not None] if not values: return "UNKNOWN" positive = sum(1 for v in values if v > 0) negative = sum(1 for v in values if v < 0) if negative > positive: return "OUTFLOW" if positive > negative: return "INFLOW" return "NEUTRAL" def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--json", default=str(DEFAULT_JSON)) 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")) liq_signal = _load(DEFAULT_LIQUIDITY_SIGNAL) liq_rows = liq_signal.get("rows") if isinstance(liq_signal.get("rows"), list) else [] liq_by_ticker: dict[str, dict] = {str(r.get("ticker") or ""): r for r in liq_rows if isinstance(r, dict)} # 기존 V2 Temp JSON 참조 (smart_money_score 소스) v2_temp = _load(ROOT / "Temp" / "smart_money_liquidity_composite_v2.json") v2_raw = v2_temp.get("rows") or _rows(h.get("smart_money_liquidity_gate_v1_json") or h.get("smart_money_liquidity_composite_v2")) if not isinstance(v2_raw, list): v2_raw = [] v2_by_ticker: dict[str, dict] = {str(r.get("ticker") or ""): r for r in v2_raw} rows_out: list[dict[str, Any]] = [] smart_money_weak_buy_count = 0 for row in df_list: ticker = str(row.get("Ticker") or row.get("ticker") or "") if not ticker: continue close = _f(row.get("Close") or row.get("close")) atv_m = _f(row.get("AvgTradeValue_20D_M")) # 슬리피지 계산 slip_bps = _slippage_bps(close, atv_m) exec_mode = _exec_mode(atv_m) # 수급 방향성 (실데이터 없으면 harness 데이터 참조) v2 = v2_by_ticker.get(ticker, {}) sm_score = _f(row.get("SmartMoney_Score") or v2.get("smart_money_score")) liq_row = liq_by_ticker.get(ticker, {}) liq_label = str(liq_row.get("liquidity_label") or "") liq_exec_mode = str(liq_row.get("execution_mode") or "") if liq_label: exec_mode = liq_exec_mode or exec_mode _, _, liq_score = _liquidity_state(atv_m) liquidity_state = liq_label else: liquidity_state, liq_exec_mode, liq_score = _liquidity_state(atv_m) if liq_exec_mode: exec_mode = liq_exec_mode foreign_div = bool(v2.get("foreign_institution_divergence_flag")) # 수급 방향성 멀티-윈도우 (gap 기반 근사) flow_1d = _f(row.get("Flow_1D")) if row.get("Flow_1D") is not None else None flow_3d = _f(row.get("Flow_3D")) if row.get("Flow_3D") is not None else None flow_5d = _f(row.get("Flow_5D")) if row.get("Flow_5D") is not None else None flow_20d = _f(row.get("Flow_20D")) if row.get("Flow_20D") is not None else None flow_direction = _flow_direction(flow_1d, flow_3d, flow_5d, flow_20d) # 수급 약세 판정 smart_money_weak = sm_score < 45.0 or (foreign_div and flow_direction == "OUTFLOW") # cash_raise 점수 상향: 수급 약세 + 유동성 양호 = 매도하기 쉬운 종목 cash_raise_priority_bonus = 1 if (smart_money_weak and liq_score >= 60.0) else 0 if smart_money_weak: smart_money_weak_buy_count += 1 rows_out.append({ "ticker": ticker, "name": str(row.get("Name") or ""), "smart_money_score": round(sm_score, 2), "liquidity_score": round(liq_score, 2), "liquidity_state": liquidity_state, "slippage_bps": slip_bps, "exec_mode": exec_mode, "avg_trade_value_20d_m": atv_m, "flow_direction": flow_direction, "flow_1d": flow_1d, "flow_3d": flow_3d, "flow_5d": flow_5d, "flow_20d": flow_20d, "foreign_institution_divergence_flag": foreign_div, "smart_money_weak": smart_money_weak, "smart_money_weak_buy_blocked": smart_money_weak, "cash_raise_priority_bonus": cash_raise_priority_bonus, "source_path": "Temp/smart_money_liquidity_composite_v3.json", "formula_id": "SMART_MONEY_LIQUIDITY_COMPOSITE_V3", }) # 수용 검증 slippage_missing_count = sum(1 for r in rows_out if r.get("slippage_bps") is None) divergence_missing_count = sum(1 for r in rows_out if r.get("foreign_institution_divergence_flag") is None) gate = "PASS" if (slippage_missing_count == 0 and smart_money_weak_buy_count == 0) else ( "WATCH" if smart_money_weak_buy_count > 0 else "CAUTION" ) # SMART_MONEY_LIQUIDITY_OUTCOME_LINK_V1: liquidity_label별 T+5 결과 조인 proposal_hist = {} hist_path = ROOT / "Temp" / "proposal_evaluation_history.json" if hist_path.exists(): try: proposal_hist = json.loads(hist_path.read_text(encoding="utf-8")) except Exception: pass hist_rows = proposal_hist.get("history") or proposal_hist.get("records") or [] if not isinstance(hist_rows, list): hist_rows = [] liq_buckets: dict = {} for h in hist_rows: if not isinstance(h, dict): continue lbl = h.get("liquidity_label") or h.get("liquidity_state") or h.get("smart_money_liquidity_label") or "UNKNOWN" t5 = h.get("realized_return_pct_t5") if t5 is None: t5 = h.get("t5_return_pct") slip = h.get("slippage_pct") if slip is None: slip = h.get("slippage_bps") if lbl not in liq_buckets: liq_buckets[lbl] = {"returns": [], "slippages": []} if t5 is not None: liq_buckets[lbl]["returns"].append(float(t5)) if slip is not None: liq_buckets[lbl]["slippages"].append(float(slip)) outcome_link_table = [] for lbl, data in liq_buckets.items(): n = len(data["returns"]) avg_ret = sum(data["returns"]) / n if n > 0 else None win_rate = sum(1 for r in data["returns"] if r > 0) / n if n > 0 else None n_slip = len(data["slippages"]) avg_slip = sum(data["slippages"]) / n_slip if n_slip > 0 else None outcome_link_table.append({ "liquidity_label": lbl, "sample_count": n, "t5_avg_return_pct": round(avg_ret, 2) if avg_ret is not None else None, "t5_win_rate": round(win_rate, 3) if win_rate is not None else None, "avg_slippage_pct": round(avg_slip, 3) if avg_slip is not None else None, "label": f"[UNVALIDATED: n={n} < 30]" if n < 30 else "VALIDATED", }) outcome_link_result = { "formula_id": "SMART_MONEY_LIQUIDITY_OUTCOME_LINK_V1", "generated_at": datetime.now(timezone.utc).isoformat(), "table": outcome_link_table, "total_linked_samples": sum(r["sample_count"] for r in outcome_link_table), "gate": "VALIDATED" if any(r["sample_count"] >= 30 for r in outcome_link_table) else "UNVALIDATED", } link_path = ROOT / "Temp" / "smart_money_liquidity_outcome_link_v1.json" link_path.write_text(json.dumps(outcome_link_result, ensure_ascii=False, indent=2), encoding="utf-8") result = { "formula_id": "SMART_MONEY_LIQUIDITY_COMPOSITE_V3", "gate": gate, "slippage_bps_missing_count": slippage_missing_count, "foreign_institution_divergence_flag_missing_count": divergence_missing_count, "smart_money_weak_buy_blocked_count": smart_money_weak_buy_count, "sell_priority_liquidity_adjusted": True, "ticker_count": len(rows_out), "rows": rows_out, "generated_at": datetime.now(timezone.utc).isoformat(), "source_path": "Temp/smart_money_liquidity_composite_v3.json", "outcome_link_ref": "Temp/smart_money_liquidity_outcome_link_v1.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") summary = {k: v for k, v in result.items() if k != "rows"} print(json.dumps(summary, indent=2, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())