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' ' if fill else ""
return (f'{area}'
f' '
f'{label} max {hi:,.0f} '
f'min {lo:,.0f} ')
def _card(t, v, cls=""):
return f'
'
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 = ('⚠️ DONNÉES SYNTHÉTIQUES : ces résultats n\'ont aucune valeur de performance réelle. '
'Ils servent uniquement à vérifier le fonctionnement du moteur.
') if synthetic else ""
wf_html = ""
if wf and wf["windows"]:
rows = "".join(
f"{w['k']} {w['test_from']} → {w['test_to']} {html.escape(str(w['params']))} "
f"{w['is_return']:+.1f}% =0 else 'bad'}'>{w['oos_return']:+.1f}% "
f"{w['oos_dd']:.1f}% {w['oos_trades']} " for w in wf["windows"])
wfe = f"{wf['wfe']:.0f} %" if wf["wfe"] is not None else "n/a"
wf_html = (f"Walk-forward (hors échantillon) Efficacité walk-forward : {wfe} "
"(au-dessus de ~50 % = stratégie plutôt robuste ; proche de 0 ou négatif = surajustement probable)
"
"# Période test Paramètres choisis IS OOS DD OOS Trades "
f"{rows}
")
mc_html = ""
if mc:
mc_html = (f"Monte Carlo ({mc['runs']} simulations) "
+ _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} %")
+ "
")
last = trades.tail(40)
trows = "".join(
f"{str(r.entry_time)[:16]} {r.side} {r.lots:.2f} {r.entry:.5g} {r.exit:.5g} "
f"=0 else 'bad'}'>{r.pnl:+.1f} {r.reason} " for r in last.itertuples())
doc = f"""
{html.escape(title)}
{html.escape(title)} {banner}{cards}
Courbe d'equity {_svg(equity.tolist(), "var(--ac)", True, label="Equity")}
Drawdown (%) {_svg([-x for x in dd], "var(--bad)", True, label="DD")}
{wf_html}{mc_html}
40 derniers trades Entrée Sens Lots Prix in Prix out PnL $ Sortie {trows}
Les performances passées ne garantissent pas les résultats futurs. Le trading comporte un risque de perte en capital.
"""
open(path, "w", encoding="utf-8").write(doc)