"""Phase 7 — Compare Python vs MT5 for finalist #1. Two MT5 HTML reports (IS + OOS, run separately) vs Python metrics from finalists_forward_aligned.json. Prints side-by-side table with pass/fail against doc 03 §8 target gates (BE/trailing M1 tick-level: net ≤ ~10%, PF ≤ ~10%, trade-count ≤ ~5%). Usage: python scripts/compare_finalist.py 1 """ from __future__ import annotations import json import sys from pathlib import Path PROJECT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT)) from shared.data.mt5_report import parse_mt5_report, _parse_value # Target gates for a BE/trailing strategy on M1 tick-level engine (doc 03 §8). GATES = { "net_profit": 0.10, "profit_factor": 0.10, "total_trades": 0.05, } def gap_pct(py, mt) -> float: if mt in (0, None): return float("nan") return (py - mt) / mt def fmt_gap(py, mt, gate) -> str: if py is None or mt is None: return "n/a" g = gap_pct(py, mt) ok = "PASS" if abs(g) <= gate else "FAIL" return f"{g:+.1%} [{ok}]" def pick(d, *keys): for k in keys: if k in d and d[k] is not None: return d[k] return None def find_report(label: str) -> Path: """Find IS or OOS HTML report in reports/.""" rdir = PROJECT / "reports" # Prefer explicit IS-/OOS- prefixed files. cands = sorted(rdir.glob(f"{label}-ReportTester*.html")) if cands: return cands[-1] # Fall back to label anywhere in the name. cands = sorted(rdir.glob(f"*{label}*.html")) if cands: return cands[-1] sys.exit(f"no {label} HTML report in {rdir}") def main() -> int: finalist_idx = int(sys.argv[1]) if len(sys.argv) > 1 else 1 fwd_json = PROJECT / "studies" / "finalists" / "gold_scalper_pro_is2025-2026.json" if not fwd_json.exists(): sys.exit(f"missing: {fwd_json} — run scripts/reeval_finalist_forward.py") fwd = json.loads(fwd_json.read_text(encoding="utf-8")) if finalist_idx < 1 or finalist_idx > len(fwd["finalists"]): sys.exit(f"finalist index must be 1..{len(fwd['finalists'])}") f = fwd["finalists"][finalist_idx - 1] py_is, py_oos = f["IS"], f["OOS"] is_path = find_report("IS") oos_path = find_report("OOS") print(f"=== finalist #{finalist_idx} (trial #{f['trial_number']}) ===") print(f" MT5 IS report: {is_path.name}") print(f" MT5 OOS report: {oos_path.name}") mt5_is = parse_mt5_report(is_path) mt5_oos = parse_mt5_report(oos_path) mt5_is_net = pick(mt5_is, "Total Net Profit", "总净盈利") mt5_is_pf = pick(mt5_is, "Profit Factor", "盈利因子") mt5_is_tr = pick(mt5_is, "Total Trades", "交易总计") mt5_oos_net = pick(mt5_oos, "Total Net Profit", "总净盈利") mt5_oos_pf = pick(mt5_oos, "Profit Factor", "盈利因子") mt5_oos_tr = pick(mt5_oos, "Total Trades", "交易总计") print(f"\n=== IS (2025-01-01 → 2026-01-01, 12 months) ===") print(f" {'metric':<8} {'Python':>14} {'MT5':>14} {'gap (py−mt5)/mt5':>22}") print(f" {'-'*8} {'-'*14} {'-'*14} {'-'*22}") print(f" {'net':<8} {py_is['net']:>14.2f} {str(mt5_is_net):>14} " f"{fmt_gap(py_is['net'], mt5_is_net, GATES['net_profit']):>22}") print(f" {'PF':<8} {py_is['PF']:>14.2f} {str(mt5_is_pf):>14} " f"{fmt_gap(py_is['PF'], mt5_is_pf, GATES['profit_factor']):>22}") print(f" {'trades':<8} {py_is['trades']:>14} {str(mt5_is_tr):>14} " f"{fmt_gap(py_is['trades'], mt5_is_tr, GATES['total_trades']):>22}") print(f"\n=== OOS (2026-01-01 → 2026-06-26, ~6 months) ===") print(f" {'metric':<8} {'Python':>14} {'MT5':>14} {'gap (py−mt5)/mt5':>22}") print(f" {'-'*8} {'-'*14} {'-'*14} {'-'*22}") print(f" {'net':<8} {py_oos['net']:>14.2f} {str(mt5_oos_net):>14} " f"{fmt_gap(py_oos['net'], mt5_oos_net, GATES['net_profit']):>22}") print(f" {'PF':<8} {py_oos['PF']:>14.2f} {str(mt5_oos_pf):>14} " f"{fmt_gap(py_oos['PF'], mt5_oos_pf, GATES['profit_factor']):>22}") print(f" {'trades':<8} {py_oos['trades']:>14} {str(mt5_oos_tr):>14} " f"{fmt_gap(py_oos['trades'], mt5_oos_tr, GATES['total_trades']):>22}") print(f"\n target gate (doc 03 §8, BE/trailing M1 tick-level): " f"net ≤10% PF ≤10% trades ≤5%") return 0 if __name__ == "__main__": raise SystemExit(main())