import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def plot_normalized_prices(prices: pd.DataFrame) -> go.Figure:
"""
Affiche les prix normalisés (base 100) des deux actifs superposés.
Parameters
----------
prices : pd.DataFrame
DataFrame avec deux colonnes de prix (index = date).
Returns
-------
go.Figure
Graphique Plotly interactif.
"""
ticker_a, ticker_b = prices.columns[0], prices.columns[1]
# Normalisation base 100
prix_normalises = prices / prices.iloc[0] * 100
fig = go.Figure()
fig.add_trace(go.Scatter(
x=prix_normalises.index,
y=prix_normalises[ticker_a],
name=ticker_a,
line=dict(color="#00d4aa", width=2),
))
fig.add_trace(go.Scatter(
x=prix_normalises.index,
y=prix_normalises[ticker_b],
name=ticker_b,
line=dict(color="#ff6b6b", width=2),
))
fig.update_layout(
title="Prix normalisés (base 100)",
xaxis_title="Date",
yaxis_title="Prix (base 100)",
template="plotly_dark",
hovermode="x unified",
height=450,
)
return fig
def plot_spread_zscore(spread: pd.Series, zscore: pd.Series, entry_threshold: float = 2.0, exit_threshold: float = 0.5) -> go.Figure:
"""
Affiche le spread et le z-score avec les zones de trading colorées.
Parameters
----------
spread : pd.Series
Le spread entre les deux actifs.
zscore : pd.Series
Le z-score rolling du spread.
entry_threshold : float
Seuil d'entrée en position.
exit_threshold : float
Seuil de sortie de position.
Returns
-------
go.Figure
Graphique Plotly avec 2 sous-graphiques (spread + z-score).
"""
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.08,
subplot_titles=("Spread", "Z-Score"),
row_heights=[0.4, 0.6],
)
# --- Spread ---
fig.add_trace(go.Scatter(
x=spread.index,
y=spread,
name="Spread",
line=dict(color="#00d4aa", width=1.5),
), row=1, col=1)
# Moyenne du spread
fig.add_hline(
y=spread.mean(), row=1, col=1,
line=dict(color="gray", dash="dash", width=1),
annotation_text="Moyenne",
)
# --- Z-Score ---
fig.add_trace(go.Scatter(
x=zscore.index,
y=zscore,
name="Z-Score",
line=dict(color="#ffffff", width=1.5),
), row=2, col=1)
# Zones de seuils
seuil_stop = 3.5
# Zone d'entrée long (z < -entry)
fig.add_hrect(
y0=-seuil_stop, y1=-entry_threshold, row=2, col=1,
fillcolor="green", opacity=0.15, line_width=0,
)
# Zone d'entrée short (z > +entry)
fig.add_hrect(
y0=entry_threshold, y1=seuil_stop, row=2, col=1,
fillcolor="red", opacity=0.15, line_width=0,
)
# Zone neutre / exit
fig.add_hrect(
y0=-exit_threshold, y1=exit_threshold, row=2, col=1,
fillcolor="gray", opacity=0.1, line_width=0,
)
# Lignes de seuils
for seuil in [entry_threshold, -entry_threshold]:
fig.add_hline(
y=seuil, row=2, col=1,
line=dict(color="yellow", dash="dot", width=1),
)
for seuil in [exit_threshold, -exit_threshold]:
fig.add_hline(
y=seuil, row=2, col=1,
line=dict(color="gray", dash="dot", width=1),
)
# Ligne zéro
fig.add_hline(y=0, row=2, col=1, line=dict(color="gray", width=0.5))
fig.update_layout(
template="plotly_dark",
hovermode="x unified",
height=600,
showlegend=False,
)
return fig
def plot_equity_curve(equity: pd.Series) -> go.Figure:
"""
Affiche la courbe d'equity du backtest.
Parameters
----------
equity : pd.Series
Courbe d'equity cumulée (P&L cumulé).
Returns
-------
go.Figure
Graphique Plotly interactif.
"""
# Coloration : vert quand positif, rouge quand négatif
couleurs = ["#00d4aa" if v >= 0 else "#ff6b6b" for v in equity]
fig = go.Figure()
fig.add_trace(go.Scatter(
x=equity.index,
y=equity,
name="Equity",
fill="tozeroy",
line=dict(color="#00d4aa", width=2),
fillcolor="rgba(0, 212, 170, 0.1)",
))
fig.add_hline(y=0, line=dict(color="gray", dash="dash", width=1))
fig.update_layout(
title="Courbe d'Equity (P&L cumulé)",
xaxis_title="Date",
yaxis_title="P&L cumulé ($)",
template="plotly_dark",
hovermode="x unified",
height=400,
)
return fig