75 lines
5.7 KiB
Python
75 lines
5.7 KiB
Python
import html
| |||
| |||
def _svg(vals, color, fill=False, w=900, h=240, label=""):
| |||
if len(vals) < 2:
| |||
return ""
| |||
step = max(1, len(vals) // 800)
| |||
v = vals[::step]
| |||
lo, hi = min(v), max(v)
| |||
rng = (hi - lo) or 1
| |||
pts = [(i / (len(v) - 1) * w, h - 18 - (x - lo) / rng * (h - 36)) for i, x in enumerate(v)]
| |||
d = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
| |||
area = f'<polygon points="0,{h-18} {d} {w},{h-18}" fill="{color}" opacity="0.15"/>' if fill else ""
| |||
return (f'<svg viewBox="0 0 {w} {h}" width="100%" style="display:block">{area}'
| |||
f'<polyline points="{d}" fill="none" stroke="{color}" stroke-width="2"/>'
| |||
f'<text x="6" y="14" font-size="12" fill="currentColor">{label} max {hi:,.0f}</text>'
| |||
f'<text x="6" y="{h-4}" font-size="12" fill="currentColor">min {lo:,.0f}</text></svg>')
| |||
| |||
def _card(t, v, cls=""):
| |||
return f'<div class="card {cls}"><div class="t">{t}</div><div class="v">{v}</div></div>'
| |||
| |||
def render_report(path, title, m, equity, trades, wf=None, mc=None, synthetic=False):
| |||
pf = "∞" if m["profit_factor"] == float("inf") else f"{m['profit_factor']:.2f}"
| |||
good = "ok" if m["net"] >= 0 else "bad"
| |||
cards = "".join([
| |||
_card("Résultat net", f"{m['net']:+,.0f} $", good), _card("Rendement", f"{m['return_pct']:+.1f} %", good),
| |||
_card("Trades", m["trades"]), _card("Winrate", f"{m['win_rate']:.0f} %"),
| |||
_card("Profit factor", pf), _card("Drawdown max", f"{m['max_dd_pct']:.1f} %", "bad" if m["max_dd_pct"] > 10 else ""),
| |||
_card("Sharpe", f"{m['sharpe']:.2f}"), _card("Espérance / trade", f"{m['expectancy']:+.1f} $"),
| |||
_card("R moyen", f"{m['avg_r']:+.2f}"), _card("Série de pertes max", m["max_loss_streak"]),
| |||
])
| |||
peak = equity.cummax()
| |||
dd = ((peak - equity) / peak * 100).tolist()
| |||
banner = ('<div class="warn">⚠️ DONNÉES SYNTHÉTIQUES : ces résultats n\'ont aucune valeur de performance réelle. '
| |||
'Ils servent uniquement à vérifier le fonctionnement du moteur.</div>') if synthetic else ""
| |||
wf_html = ""
| |||
if wf and wf["windows"]:
| |||
rows = "".join(
| |||
f"<tr><td>{w['k']}</td><td>{w['test_from']} → {w['test_to']}</td><td>{html.escape(str(w['params']))}</td>"
| |||
f"<td>{w['is_return']:+.1f}%</td><td class='{'ok' if w['oos_return']>=0 else 'bad'}'>{w['oos_return']:+.1f}%</td>"
| |||
f"<td>{w['oos_dd']:.1f}%</td><td>{w['oos_trades']}</td></tr>" for w in wf["windows"])
| |||
wfe = f"{wf['wfe']:.0f} %" if wf["wfe"] is not None else "n/a"
| |||
wf_html = (f"<h2>Walk-forward (hors échantillon)</h2><p>Efficacité walk-forward : <b>{wfe}</b> "
| |||
"(au-dessus de ~50 % = stratégie plutôt robuste ; proche de 0 ou négatif = surajustement probable)</p>"
| |||
"<div class='sc'><table><tr><th>#</th><th>Période test</th><th>Paramètres choisis</th><th>IS</th><th>OOS</th><th>DD OOS</th><th>Trades</th></tr>"
| |||
f"{rows}</table></div>")
| |||
mc_html = ""
| |||
if mc:
| |||
mc_html = (f"<h2>Monte Carlo ({mc['runs']} simulations)</h2><div class='grid'>"
| |||
+ _card("Capital final (pire 5 %)", f"{mc['final_p5']:,.0f} $") + _card("Capital final (médian)", f"{mc['final_p50']:,.0f} $")
| |||
+ _card("Capital final (meilleur 5 %)", f"{mc['final_p95']:,.0f} $") + _card("DD médian", f"{mc['dd_p50']:.1f} %")
| |||
+ _card("DD dans 95 % des cas ≤", f"{mc['dd_p95']:.1f} %") + _card("Proba. d'être en perte", f"{mc['prob_loss']:.0f} %")
| |||
+ _card("Proba. DD ≥ 10 % (limite prop)", f"{mc['prob_dd10']:.0f} %") + _card(f"Proba. DD ≥ {mc['ruin_dd_pct']:.0f} %", f"{mc['prob_ruin']:.0f} %")
| |||
+ "</div>")
| |||
last = trades.tail(40)
| |||
trows = "".join(
| |||
f"<tr><td>{str(r.entry_time)[:16]}</td><td>{r.side}</td><td>{r.lots:.2f}</td><td>{r.entry:.5g}</td><td>{r.exit:.5g}</td>"
| |||
f"<td class='{'ok' if r.pnl>=0 else 'bad'}'>{r.pnl:+.1f}</td><td>{r.reason}</td></tr>" for r in last.itertuples())
| |||
doc = f"""<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
| |||
<title>{html.escape(title)}</title><style>
| |||
:root{{--bg:#fff;--fg:#14181f;--card:#f3f5f8;--ok:#0a8f4d;--bad:#d1342f;--ac:#2563eb}}
| |||
@media(prefers-color-scheme:dark){{:root{{--bg:#0f1319;--fg:#e6e9ee;--card:#1b222c;--ok:#34d399;--bad:#f87171;--ac:#60a5fa}}}}
| |||
body{{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif;margin:0;padding:16px;max-width:980px;margin:auto}}
| |||
h1{{font-size:20px}}h2{{font-size:17px;margin-top:28px}}.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}}
| |||
.card{{background:var(--card);border-radius:10px;padding:10px}}.t{{font-size:12px;opacity:.7}}.v{{font-size:18px;font-weight:600}}
| |||
.ok .v,td.ok{{color:var(--ok)}}.bad .v,td.bad{{color:var(--bad)}}.warn{{background:#fff3cd;color:#664d03;padding:10px;border-radius:8px;margin:10px 0}}
| |||
.sc{{overflow-x:auto}}table{{border-collapse:collapse;width:100%;font-size:13px}}td,th{{padding:5px 8px;border-bottom:1px solid var(--card);text-align:left;white-space:nowrap}}
| |||
.risk{{font-size:12px;opacity:.7;margin-top:30px}}</style></head><body>
| |||
<h1>{html.escape(title)}</h1>{banner}<div class="grid">{cards}</div>
| |||
<h2>Courbe d'equity</h2>{_svg(equity.tolist(), "var(--ac)", True, label="Equity")}
| |||
<h2>Drawdown (%)</h2>{_svg([-x for x in dd], "var(--bad)", True, label="DD")}
| |||
{wf_html}{mc_html}
| |||
<h2>40 derniers trades</h2><div class="sc"><table><tr><th>Entrée</th><th>Sens</th><th>Lots</th><th>Prix in</th><th>Prix out</th><th>PnL $</th><th>Sortie</th></tr>{trows}</table></div>
| |||
<p class="risk">Les performances passées ne garantissent pas les résultats futurs. Le trading comporte un risque de perte en capital.</p>
| |||
</body></html>"""
| |||
open(path, "w", encoding="utf-8").write(doc)
|