pairs-cointegration / src / data.py
data.py
Raw
import pandas as pd
import yfinance as yf


def fetch_pair_data(ticker_a: str, ticker_b: str, period: str = "5y") -> pd.DataFrame:
    """
    Récupère les prix adjusted close des deux tickers via yfinance.

    Retourne un DataFrame avec colonnes [ticker_a, ticker_b], index = date.
    Gère les cas d'erreur : ticker invalide, données manquantes.
    Supprime les lignes avec NaN (jours où un seul ticker a coté).

    Parameters
    ----------
    ticker_a : str
        Premier ticker (ex: "KO").
    ticker_b : str
        Second ticker (ex: "PEP").
    period : str
        Période historique à récupérer (ex: "3y", "5y", "10y").

    Returns
    -------
    pd.DataFrame
        DataFrame avec colonnes [ticker_a, ticker_b] et index DatetimeIndex.

    Raises
    ------
    ValueError
        Si un ticker est invalide ou si les données sont insuffisantes.
    """
    ticker_a = ticker_a.strip().upper()
    ticker_b = ticker_b.strip().upper()

    # Téléchargement des données via yfinance
    donnees_brutes = yf.download(
        tickers=[ticker_a, ticker_b],
        period=period,
        auto_adjust=True,
        progress=False,
    )

    # Vérification que le téléchargement a fonctionné
    if donnees_brutes.empty:
        raise ValueError(
            f"Aucune donnée récupérée pour {ticker_a} et/ou {ticker_b}. "
            "Vérifiez que les tickers sont valides."
        )

    # Extraction des prix de clôture
    prix_cloture = donnees_brutes["Close"]

    # Vérification que les deux tickers sont présents dans les colonnes
    colonnes_presentes = prix_cloture.columns.tolist()
    for ticker in [ticker_a, ticker_b]:
        if ticker not in colonnes_presentes:
            raise ValueError(
                f"Le ticker '{ticker}' n'a retourné aucune donnée. "
                "Vérifiez qu'il est valide sur Yahoo Finance."
            )

    # Sélection et renommage des colonnes
    prix_paire = prix_cloture[[ticker_a, ticker_b]].copy()

    # Suppression des lignes avec des NaN (jours où un seul ticker a coté)
    nb_lignes_avant = len(prix_paire)
    prix_paire = prix_paire.dropna()
    nb_lignes_supprimees = nb_lignes_avant - len(prix_paire)

    if nb_lignes_supprimees > 0:
        print(
            f"Info : {nb_lignes_supprimees} lignes supprimées (données manquantes)."
        )

    # Vérification qu'il reste assez de données pour une analyse significative
    nb_jours_minimum = 252  # ~1 an de trading
    if len(prix_paire) < nb_jours_minimum:
        raise ValueError(
            f"Seulement {len(prix_paire)} jours de données disponibles "
            f"(minimum requis : {nb_jours_minimum}). "
            "Essayez une période plus longue ou vérifiez les tickers."
        )

    return prix_paire