pairs-cointegration / app.py
app.py
Raw
import streamlit as st
import pandas as pd

from src.data import fetch_pair_data
from src.cointegration import test_cointegration, compute_spread, compute_zscore
from src.signals import generate_signals
from src.backtest import run_backtest
from src.visualization import plot_normalized_prices, plot_spread_zscore, plot_equity_curve


# --- Configuration de la page ---
st.set_page_config(
    page_title="Pairs Trading — Cointegration Dashboard",
    layout="wide",
)

st.title("Pairs Trading — Cointegration Dashboard")
st.caption("Analyse de cointégration et backtest de stratégies de pairs trading")

# --- Initialisation du session_state pour les tickers ---
if "ticker_a" not in st.session_state:
    st.session_state.ticker_a = "KO"
if "ticker_b" not in st.session_state:
    st.session_state.ticker_b = "PEP"

# --- Sidebar : paramètres ---
with st.sidebar:
    st.header("Paramètres")

    periode = st.selectbox("Période de données", ["3y", "5y", "10y", "15y", "20y", "max"], index=1)

    entry_threshold = st.slider(
        "Z-score seuil d'entrée",
        min_value=1.5, max_value=3.0, value=2.0, step=0.1,
    )

    exit_threshold = st.slider(
        "Z-score seuil de sortie",
        min_value=0.0, max_value=1.0, value=0.5, step=0.1,
    )

    rolling_window = st.slider(
        "Fenêtre rolling (jours)",
        min_value=20, max_value=120, value=60, step=5,
    )

    transaction_cost = st.slider(
        "Coût de transaction (%)",
        min_value=0.0, max_value=0.5, value=0.1, step=0.05,
    )

# --- Sélection des tickers ---
# --- Paires suggérées ---
st.markdown("**Paires suggérées :**")
paires_suggerees = {
    "V / MA": ("V", "MA"),
    "MCD / YUM": ("MCD", "YUM"),
    "PG / CL": ("PG", "CL"),
    "XOM / CVX": ("XOM", "CVX"),
    "KO / PEP": ("KO", "PEP"),
}

colonnes_paires = st.columns(len(paires_suggerees))

for i, (nom, (ta, tb)) in enumerate(paires_suggerees.items()):
    with colonnes_paires[i]:
        if st.button(nom, use_container_width=True):
            st.session_state.ticker_a = ta
            st.session_state.ticker_b = tb
            st.rerun()

# --- Sélection des tickers ---
col_input_a, col_input_b, col_btn = st.columns([2, 2, 1])

with col_input_a:
    ticker_a = st.text_input("Ticker A", key="ticker_a")
with col_input_b:
    ticker_b = st.text_input("Ticker B", key="ticker_b")
with col_btn:
    st.write("")  # Espacement vertical
    st.write("")
    bouton_analyser = st.button("Analyser", type="primary", use_container_width=True)

# --- Analyse ---
if bouton_analyser and ticker_a and ticker_b:
    st.divider()

    # Récupération des données
    with st.spinner(f"Récupération des données pour {ticker_a} / {ticker_b}..."):
        try:
            prix = fetch_pair_data(ticker_a, ticker_b, period=periode)
        except ValueError as e:
            st.error(f"Erreur : {e}")
            st.stop()

    # Test de cointégration
    resultats_coint = test_cointegration(prix[ticker_a], prix[ticker_b])

    # --- Résultats du test ---
    st.header("Résultats du test de cointégration")

    col_stat, col_pval, col_beta = st.columns(3)
    with col_stat:
        valeur_critique_5 = resultats_coint["critical_values"]["5%"]
        st.metric(
            "Statistique de test",
            f"{resultats_coint['test_statistic']:.4f}",
            delta=f"seuil 5% : {valeur_critique_5:.4f}",
            delta_color="off",
        )
    with col_pval:
        st.metric("P-value", f"{resultats_coint['p_value']:.6f}")
    with col_beta:
        st.metric("Beta (hedge ratio)", f"{resultats_coint['beta']:.4f}")

    if resultats_coint["is_cointegrated"]:
        st.success(
            f"✅ **La paire {ticker_a}/{ticker_b} est cointégrée** (p-value = {resultats_coint['p_value']:.4f}). "
            "Le spread montre un comportement de retour à la moyenne — les signaux de pairs trading sont exploitables."
        )
    else:
        st.warning(
            f"⚠️ **La paire {ticker_a}/{ticker_b} n'est PAS cointégrée** (p-value = {resultats_coint['p_value']:.4f}). "
            "Le spread ne montre pas de comportement de retour à la moyenne. "
            "Les signaux de pairs trading seraient peu fiables. Essayez une autre paire ou ajustez la période."
        )

    # Valeurs critiques
    with st.expander("Valeurs critiques du test"):
        for niveau, valeur in resultats_coint["critical_values"].items():
            st.write(f"**{niveau}** : {valeur}")

    # --- Graphique 1 : Prix normalisés ---
    st.header("Prix normalisés (base 100)")
    fig_prix = plot_normalized_prices(prix)
    st.plotly_chart(fig_prix, use_container_width=True)

    # --- Calcul du spread et z-score ---
    beta = resultats_coint["beta"]
    spread = compute_spread(prix[ticker_a], prix[ticker_b], beta)
    zscore = compute_zscore(spread, window=rolling_window)

    # --- Graphique 2 : Spread & Z-Score ---
    st.header("Spread & Z-Score")
    fig_spread = plot_spread_zscore(spread, zscore, entry_threshold, exit_threshold)
    st.plotly_chart(fig_spread, use_container_width=True)

    # --- Backtest (uniquement si cointégrée ou si l'utilisateur veut quand même) ---
    if not resultats_coint["is_cointegrated"]:
        afficher_backtest = st.checkbox(
            "Afficher le backtest quand même (résultats peu fiables)", value=False
        )
    else:
        afficher_backtest = True

    if afficher_backtest:
        # Génération des signaux
        signals = generate_signals(zscore, entry_threshold, exit_threshold)

        # Backtest
        resultats_bt = run_backtest(
            prix[ticker_a], prix[ticker_b],
            signals, beta,
            transaction_cost=transaction_cost / 100,
        )

        # --- Graphique 3 : Equity Curve ---
        st.header("Courbe d'Equity (Backtest)")
        fig_equity = plot_equity_curve(resultats_bt["equity_curve"])
        st.plotly_chart(fig_equity, use_container_width=True)

        # --- Métriques de performance ---
        st.header("Métriques de performance")

        col1, col2, col3, col4 = st.columns(4)
        with col1:
            st.metric("Rendement total", f"{resultats_bt['total_return_pct']:.1f}%")
            st.metric("Rendement annualisé", f"{resultats_bt['annualized_return_pct']:.1f}%")
        with col2:
            st.metric("Sharpe Ratio", f"{resultats_bt['sharpe_ratio']:.2f}")
            st.metric("Profit Factor", f"{resultats_bt['profit_factor']:.2f}")
        with col3:
            st.metric("Max Drawdown", f"{resultats_bt['max_drawdown']:.2f} $")
            st.metric("Win Rate", f"{resultats_bt['win_rate']:.1f}%")
        with col4:
            st.metric("Nombre de trades", f"{resultats_bt['num_trades']}")
            st.metric("Durée moyenne", f"{resultats_bt['avg_trade_duration']:.0f} jours")

        # --- Log des trades ---
        st.header("Log des trades")

        if resultats_bt["trade_log"]:
            df_trades = pd.DataFrame(resultats_bt["trade_log"])
            df_trades["date_entree"] = pd.to_datetime(df_trades["date_entree"]).dt.date
            df_trades["date_sortie"] = pd.to_datetime(df_trades["date_sortie"]).dt.date

            # Coloration des lignes selon le P&L
            st.dataframe(
                df_trades.style.map(
                    lambda v: "color: #00d4aa" if isinstance(v, (int, float)) and v > 0
                    else ("color: #ff6b6b" if isinstance(v, (int, float)) and v < 0 else ""),
                    subset=["pnl"],
                ),
                use_container_width=True,
                hide_index=True,
            )
        else:
            st.info("Aucun trade détecté sur cette période.")

# --- Footer ---
st.divider()
st.caption("© 2026 — Pairs Trading Dashboard | Développé par Celestin Guilhen | Données fournies par Yahoo Finance")