"""Python-vs-MT5 comparison table (doc 07 §8). For every finalist, write an ``auto-verification.md`` that puts the two tiers side by side and judges the delta against the expected fidelity gap (doc 03 §7): a clean-directional setup shows only a modest negative gap (MT5 a little below Python); a trailing/grid setup in volatile history can gap much wider, and that's *expected*, not a bug. Decision rule: if the **MT5** number still clears your bar after the gap, the finalist is real; if the edge only existed in the optimistic Python figure, discard it. """ from __future__ import annotations from pathlib import Path from typing import Mapping from ..core.metrics import Metrics # Rows shown in the comparison table (doc 07 §8). COMPARISON_ROWS: list[tuple[str, str, str]] = [ ("net_profit", "Net", "{:,.2f}"), ("profit_factor", "Profit Factor", "{:.2f}"), ("max_equity_dd", "Equity DD max", "{:,.2f}"), ("total_trades", "Total trades", "{:d}"), ("win_rate", "Win rate", "{:.2%}"), ("sharpe", "Sharpe", "{:.2f}"), ] def build_comparison_table( py_metrics: Metrics | Mapping[str, object], mt5_metrics: Mapping[str, object], *, rows: list[tuple[str, str, str]] | None = None, ) -> str: """Build the Python-vs-MT5 markdown comparison table. ``py_metrics`` may be a :class:`Metrics` dataclass or a mapping. MT5 metrics come from :func:`shared.data.mt5_report.parse_mt5_report`. The ``Δ`` column is the relative difference where both values are numeric. """ rows = rows or COMPARISON_ROWS py = _as_mapping(py_metrics) lines = [ "| Metric | Python | MT5 | Δ |", "|--------|--------|-----|---|", ] for key, label, fmt in rows: pv = py.get(key) mv = mt5_metrics.get(label) or mt5_metrics.get(key) p_str = _fmt(pv, fmt) m_str = _fmt(mv, fmt) delta = _delta(pv, mv) lines.append(f"| {label} | {p_str} | {m_str} | {delta} |") return "\n".join(lines) + "\n" def write_comparison( py_metrics: Metrics | Mapping[str, object], mt5_metrics: Mapping[str, object], path: str | Path, *, notes: str = "", rows: list[tuple[str, str, str]] | None = None, ) -> None: """Write the comparison table + notes to an ``auto-verification.md``.""" p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) table = build_comparison_table(py_metrics, mt5_metrics, rows=rows) body = "# Auto-verification: Python vs MT5\n\n" + table if notes: body += "\n\n## Notes\n\n" + notes + "\n" body += ( "\n## Decision rule (doc 03 §7)\n" "If the MT5 number still clears the bar after the expected fidelity " "gap, the finalist is real. If the edge only existed in the optimistic " "Python figure, discard it.\n" ) p.write_text(body, encoding="utf-8") def _as_mapping(metrics: Metrics | Mapping[str, object]) -> Mapping[str, object]: if isinstance(metrics, Mapping): return metrics return {k: getattr(metrics, k) for k, _ in COMPARISON_ROWS if hasattr(metrics, k)} def _fmt(v: object, fmt: str) -> str: if v is None: return "—" if isinstance(v, (int, float)) and fmt: try: return fmt.format(v) except (ValueError, TypeError): return str(v) return str(v) def _delta(py: object, mt5: object) -> str: if not isinstance(py, (int, float)) or not isinstance(mt5, (int, float)): return "—" if py == 0: return "—" pct = (mt5 - py) / abs(py) * 100.0 return f"{pct:+.1f}%"