pairs-cointegration / src / visualization.py
visualization.py
Raw
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