"""SMART_MONEY_LIQUIDITY_GATE_V1 스마트머니·유동성 차단 게이트 — SM001 / SM002 / SM003 결정론 구현. SM001: inst_5d < 0 AND frg_5d < 0 → BLOCK_BUY SM002: avg_trade_value_5d_m < 5000 (50억 = 5,000M KRW) → LIMIT_QUANTITY SM003: rsi14 > 70 AND flow_credit < 0.3 → BLOCK_BUY gate_status 우선순위: BLOCK_BUY > LIMIT_QUANTITY > PASS """ 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_SIGNAL = ROOT / "Temp" / "smart_money_flow_signal_v2.json" DEFAULT_OUT = ROOT / "Temp" / "smart_money_liquidity_gate_v1.json" # SM002 임계값: 50억 = 5,000,000,000 KRW = 5,000 M KRW AVG_TRADE_VALUE_5D_M_THRESHOLD = 5_000.0 # SM003 임계값 RSI14_THRESHOLD = 70.0 FLOW_CREDIT_THRESHOLD = 0.3 def _load_json(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 _as_float(v: Any, default: float | None = None) -> float | None: """None-safe float 변환. 변환 불가 시 default 반환.""" if v is None: return default try: return float(v) except Exception: return default 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 _check_sm001(row: dict[str, Any]) -> dict[str, Any] | None: """SM001: 외국인 5D + 기관 5D 동시 순매도 → BLOCK_BUY.""" frg = _as_float(row.get("Frg_5D")) inst = _as_float(row.get("Inst_5D")) if frg is None or inst is None: return None # 데이터 부재 — silent PASS 금지, 호출자에서 missing 처리 if frg < 0 and inst < 0: return { "rule": "SM001", "condition": "inst_5d<0 AND frg_5d<0", "values": {"inst_5d": inst, "frg_5d": frg}, "result": "BLOCK_BUY", } return None def _check_sm002(row: dict[str, Any]) -> dict[str, Any] | None: """SM002: 5일 평균 거래대금 < 50억(5,000M KRW) → LIMIT_QUANTITY.""" avg_tv = _as_float(row.get("AvgTradeValue_5D_M")) if avg_tv is None: return None if avg_tv < AVG_TRADE_VALUE_5D_M_THRESHOLD: return { "rule": "SM002", "condition": f"avg_trade_value_5d_m<{AVG_TRADE_VALUE_5D_M_THRESHOLD}", "values": {"avg_trade_value_5d_m": avg_tv}, "result": "LIMIT_QUANTITY", } return None def _check_sm003(row: dict[str, Any]) -> dict[str, Any] | None: """SM003: RSI14 > 70 AND flow_credit < 0.3 → BLOCK_BUY (과매수+자금 이탈).""" rsi = _as_float(row.get("RSI14")) fc = _as_float(row.get("Flow_Credit")) if rsi is None or fc is None: return None if rsi > RSI14_THRESHOLD and fc < FLOW_CREDIT_THRESHOLD: return { "rule": "SM003", "condition": f"rsi14>{RSI14_THRESHOLD} AND flow_credit<{FLOW_CREDIT_THRESHOLD}", "values": {"rsi14": rsi, "flow_credit": fc}, "result": "BLOCK_BUY", } return None def _evaluate_ticker(row: dict[str, Any]) -> dict[str, Any]: ticker = str(row.get("Ticker") or "UNKNOWN") name = str(row.get("Name") or "") rules_fired: list[dict[str, Any]] = [] missing_fields: list[str] = [] sm1 = _check_sm001(row) if sm1 is not None: rules_fired.append(sm1) else: # 명시적 데이터 부재 추적 frg = row.get("Frg_5D") inst = row.get("Inst_5D") if frg is None: missing_fields.append("Frg_5D") if inst is None: missing_fields.append("Inst_5D") sm2 = _check_sm002(row) if sm2 is not None: rules_fired.append(sm2) else: if row.get("AvgTradeValue_5D_M") is None: missing_fields.append("AvgTradeValue_5D_M") sm3 = _check_sm003(row) if sm3 is not None: rules_fired.append(sm3) else: if row.get("RSI14") is None: missing_fields.append("RSI14") if row.get("Flow_Credit") is None: missing_fields.append("Flow_Credit") # gate_status 결정: BLOCK_BUY > LIMIT_QUANTITY > PASS results_set = {r["result"] for r in rules_fired} if "BLOCK_BUY" in results_set: gate_status = "BLOCK_BUY" elif "LIMIT_QUANTITY" in results_set: gate_status = "LIMIT_QUANTITY" elif missing_fields: gate_status = "DATA_MISSING" # 판단 불가 — PASS로 처리하지 않음 else: gate_status = "PASS" return { "ticker": ticker, "name": name, "gate_status": gate_status, "rules_fired": rules_fired, "missing_fields": missing_fields, "formula_id": "SMART_MONEY_LIQUIDITY_GATE_V1", } def main() -> int: _ensure_utf8_stdio() ap = argparse.ArgumentParser(description="스마트머니·유동성 차단 게이트 V1 (SM001~SM003)") ap.add_argument("--json", default=str(DEFAULT_JSON)) ap.add_argument("--sm-signal", default=str(DEFAULT_SM_SIGNAL)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() json_path = Path(args.json) if not json_path.is_absolute(): json_path = ROOT / json_path out_path = Path(args.out) if not out_path.is_absolute(): out_path = ROOT / out_path data = _load_json(json_path) feed = _extract_data_feed(data) if not feed: print("ERROR: data_feed 비어 있음 — GatherTradingData.json 경로 확인", file=sys.stderr) return 1 rows: list[dict[str, Any]] = [] block_count = 0 limit_count = 0 pass_count = 0 missing_count = 0 for row in feed: result = _evaluate_ticker(row) rows.append(result) gs = result["gate_status"] if gs == "BLOCK_BUY": block_count += 1 elif gs == "LIMIT_QUANTITY": limit_count += 1 elif gs == "DATA_MISSING": missing_count += 1 else: pass_count += 1 # SM 규칙별 발화 집계 sm001_fired = sum(1 for r in rows if any(f["rule"] == "SM001" for f in r["rules_fired"])) sm002_fired = sum(1 for r in rows if any(f["rule"] == "SM002" for f in r["rules_fired"])) sm003_fired = sum(1 for r in rows if any(f["rule"] == "SM003" for f in r["rules_fired"])) coverage_pct = round(100.0 * len(rows) / max(1, len(feed)), 2) out = { "formula_id": "SMART_MONEY_LIQUIDITY_GATE_V1", "gate": "OK", "coverage_pct": coverage_pct, "ticker_count": len(rows), "summary": { "block_buy_count": block_count, "limit_quantity_count": limit_count, "pass_count": pass_count, "data_missing_count": missing_count, "sm001_fired": sm001_fired, "sm002_fired": sm002_fired, "sm003_fired": sm003_fired, }, "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(f"SMART_MONEY_LIQUIDITY_GATE_V1") print(f" tickers: {len(rows)}/{len(feed)} (coverage={coverage_pct}%)") print(f" BLOCK_BUY: {block_count} LIMIT_QUANTITY: {limit_count} PASS: {pass_count} DATA_MISSING: {missing_count}") print(f" SM001(동시순매도): {sm001_fired}건 SM002(저유동성): {sm002_fired}건 SM003(과매수+이탈): {sm003_fired}건") for r in rows: fired_str = ", ".join(f['rule'] for f in r['rules_fired']) or "NONE" print(f" [{r['ticker']}] {r['gate_status']:15s} rules={fired_str}") return 0 if __name__ == "__main__": raise SystemExit(main())