118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
|
|
"""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())
|