Files
2026-06-26 20:50:07 +08:00

107 lines
3.5 KiB
Python

"""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}%"