107 lines
3.5 KiB
Python
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}%"
|