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'
{t}
{v}
' 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)

" "
" f"{rows}
#Période testParamètres choisisISOOSDD OOSTrades
") 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

{trows}
EntréeSensLotsPrix inPrix outPnL $Sortie

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)