from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] TEMP = ROOT / "Temp" DEFAULT_CAPITAL = TEMP / "capital_style_allocation_v1.json" DEFAULT_HORIZON = TEMP / "horizon_classification_v1.json" DEFAULT_FUND = TEMP / "fundamental_multifactor_v3.json" DEFAULT_OUT = TEMP / "unified_route_packet_v1.json" FORMULA_ID = "UNIFIED_ROUTE_PACKET_V1" VALID_STYLES = ("SCALP", "SWING", "MOMENTUM", "POSITION") 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 _f(v: Any, default: float = 0.0) -> float: try: return float(v) except Exception: return default def _best_style(row: dict[str, Any]) -> dict[str, Any]: styles = row.get("styles") or [] best = max( [s for s in styles if isinstance(s, dict)], key=lambda s: _f(s.get("conviction_score")), default={}, ) return best if isinstance(best, dict) else {} def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--capital", default=str(DEFAULT_CAPITAL)) ap.add_argument("--horizon", default=str(DEFAULT_HORIZON)) ap.add_argument("--fund", default=str(DEFAULT_FUND)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() capital_path = Path(args.capital) horizon_path = Path(args.horizon) fund_path = Path(args.fund) out_path = Path(args.out) if not capital_path.is_absolute(): capital_path = ROOT / capital_path if not horizon_path.is_absolute(): horizon_path = ROOT / horizon_path if not fund_path.is_absolute(): fund_path = ROOT / fund_path if not out_path.is_absolute(): out_path = ROOT / out_path capital = _load(capital_path) horizon = _load(horizon_path) fund = _load(fund_path) fund_rows = {str(r.get("ticker") or ""): r for r in (fund.get("rows") or []) if isinstance(r, dict)} hz_rows = {str(r.get("ticker") or ""): r for r in (horizon.get("rows") or []) if isinstance(r, dict)} rows_out: list[dict[str, Any]] = [] blocked_count = 0 style_score_range_violations = 0 every_ticker_has_one_best_style = True blocked_reason_codes_non_empty_when_blocked = True for row in capital.get("rows") or []: if not isinstance(row, dict): continue ticker = str(row.get("ticker") or "") name = str(row.get("name") or "") sb = row.get("signal_breakdown") or {} best = _best_style(row) best_style = str(best.get("style") or "UNKNOWN") conviction = _f(best.get("conviction_score")) recommended_pct = _f(best.get("recommended_pct")) actual_horizon = str(hz_rows.get(ticker, {}).get("horizon") or "UNKNOWN") expected_horizon = {"SCALP": "SHORT", "SWING": "SHORT", "MOMENTUM": "MID", "POSITION": "LONG"}.get(best_style, "UNKNOWN") buy_allowed = bool((fund_rows.get(ticker) or {}).get("buy_allowed")) liquidity_label = str(sb.get("liquidity_label") or "UNKNOWN") macro_gate = str(sb.get("macro_gate") or "UNKNOWN") blocked_reason_codes: list[str] = [] if not best_style or best_style not in VALID_STYLES: every_ticker_has_one_best_style = False blocked_reason_codes.append("BEST_STYLE_MISSING") if not (0.0 <= conviction <= 100.0): style_score_range_violations += 1 blocked_reason_codes.append("CONVICTION_RANGE") if liquidity_label == "FROZEN": blocked_reason_codes.append("LIQUIDITY_FROZEN") if macro_gate == "AVOID_NEW_BUY": blocked_reason_codes.append("MACRO_AVOID_NEW_BUY") if not buy_allowed: blocked_reason_codes.append("FUNDAMENTAL_BUY_BLOCK") if expected_horizon != "UNKNOWN" and actual_horizon not in ("UNKNOWN", "ETF") and expected_horizon != actual_horizon: blocked_reason_codes.append("STYLE_HORIZON_MISMATCH") if conviction < 35.0: blocked_reason_codes.append("CONVICTION_LT_35") blocked = len(blocked_reason_codes) > 0 if blocked: blocked_count += 1 if not blocked_reason_codes: blocked_reason_codes_non_empty_when_blocked = False rows_out.append({ "ticker": ticker, "name": name, "best_style": best_style, "best_style_conviction_score": round(conviction, 2), "recommended_pct": recommended_pct, "expected_horizon": expected_horizon, "actual_horizon": actual_horizon, "blocked": blocked, "blocked_reason_codes": blocked_reason_codes, "signal_breakdown": sb, "formula_id": FORMULA_ID, }) result = { "formula_id": FORMULA_ID, "gate": "PASS" if every_ticker_has_one_best_style and style_score_range_violations == 0 and blocked_reason_codes_non_empty_when_blocked else "FAIL", "ticker_count": len(rows_out), "blocked_count": blocked_count, "every_ticker_has_one_best_style": every_ticker_has_one_best_style, "every_style_score_range_0_100": style_score_range_violations == 0, "blocked_reason_codes_non_empty_when_blocked": blocked_reason_codes_non_empty_when_blocked, "rows": rows_out, "source": { "capital_style_allocation_v1_json": str(capital_path), "horizon_classification_v1_json": str(horizon_path), "fundamental_multifactor_v3_json": str(fund_path), }, } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())