63a829cc46
主要内容: - Phase 8 PROMOTE: finalist #1 (trial #324) registry 条目,自动生成 - Optuna objective warmup bug 修复 (shared/optimizer/objective.py) - studies/ 目录按用途重组为 optuna/ + finalists/ + features/ 三层 - reports/ 加入 Optuna 中文 dashboard (5 主图 + 18 slice + 15 contour) - 新增 PROJECT_GUIDE.md 项目说明文档 - 新增 build_registry_entry.py / build_optuna_dashboard.py / build_feature_datasets.py - .gitignore: 允许提交 studies/*.db (Optuna DB) 和 reports/*.html (MT5 + dashboard)
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())
|