Building a Securities Master — Part 4 de 6

Corporate Actions & Mathématiques d'Ajustement

Pour un modèle IA, un split 4-pour-1 ressemble à un crash de 75%. Maîtrisez la taxonomie complète des corporate actions, le calcul des facteurs cumulatifs EXP(SUM(LN())), et leur intégration dans la couche Gold dbt.

Stock Splits Dividendes Fusions & Spin-offs CRAF SQL
Securities Master 4/6
Le Problème Taxonomie Splits Dividendes Spin-offs & Fusions Intégration Gold Sources de Données Quiz

Dans les parties précédentes, nous avons construit un pipeline robuste : les données OHLCV arrivent proprement en Silver, les snapshots SCD2 tracent l'historique des attributs. Mais notre backtester et notre moteur IA vont encore perdre de l'argent. Pourquoi ? Parce que le prix unadjusté d'Apple est passé de ~$500 à ~$125 du jour au lendemain en août 2020 — sans que rien ne change fondamentalement.

Le Crash Fantôme d'Apple — Août 2020

Apple (AAPL) a exécuté un split 4-pour-1 le 31 août 2020. Le prix clôture à ~$499 le vendredi 28 août. Le lundi 31 août, il ouvre à ~$127. Si votre algorithme de momentum calcule un rendement sur 3 jours : (127 - 499) / 499 = -74.5%. Signal de vente catastrophique généré automatiquement.

Sans ajustement, tous vos indicateurs techniques (RSI, Bollinger Bands, SMA crossovers) sont corrompus sur toute la période précédant le split. Votre backtester croit avoir découvert une opportunité de short fantastique. Un fonds qui tradait sur des données unadjustées a perdu des millions ainsi.

AAPL — Prix unadjusté vs. ajusté autour du split 4:1 d'août 2020
Taxonomie Complète

Les 5 Familles de Corporate Actions

Chaque type de corporate action a un impact mathématique différent sur les prix historiques. Les ignorer ou les traiter incorrectement est la principale source de biais de look-ahead dans les backtests institutionnels.

Stock Split

Division du nombre d'actions. Prix ajusté = prix / ratio. Les actions existantes sont multipliées, la valeur totale reste identique.

Exemple : AAPL 4:1 (2020)
TSLA 3:1 (2022)

Dividende Cash

Distribution de cash aux actionnaires. Le prix ex-dividende baisse du montant du dividende. Ajustement rétroactif de l'historique.

Exemple : MSFT 0.75$/action
Dividende trimestriel

Spin-off

Scission d'une filiale en société indépendante. L'action mère perd de la valeur ; une nouvelle action naît. Deux séries de prix à gérer simultanément.

Exemple : PayPal / eBay (2015)
Kyndryl / IBM (2021)

Fusion / Acquisition

Un ticker cesse d'exister et est absorbé par un autre. Les actionnaires reçoivent des actions de l'acquéreur, du cash, ou un mélange des deux.

Exemple : Twitter → X (2022)
Activision → MSFT (2023)

Delisting

L'action est retirée de la cote. Faillite, privatisation, ou retrait volontaire. Le dernier prix valide est préservé mais l'action n'est plus tradable.

Exemple : SVB Financial (2023)
Bais de Survivorship
Type Impact Prix Facteur d'Ajustement Direction Fréquence
Split N:1 Prix ÷ N 1/N (ex: 1/4 pour 4:1) Prix baisse, volume monte Rare (<1/an)
Reverse Split 1:N Prix × N N (ex: 10 pour 1:10) Prix monte, volume baisse Très rare
Dividende Cash D$ Prix − D (P − D) / P Prix légèrement baisse Trimestriel à annuel
Dividende + Split Combiné Facteur composite Les deux effets Voir Yahoo CRAF
Spin-off (ratio r) Prix × (1 − r) (1 − valeur_spin/prix) Prix mère baisse Très rare
Fusion (tout cash) Série arrêtée N/A (dernier prix) Delisting Variable
Stock Splits

Calcul des Facteurs d'Ajustement Cumulatifs

La clé de l'ajustement de splits est le Cumulative Ratio Adjustment Factor (CRAF). Pour chaque barre historique, nous calculons le produit cumulatif de tous les ratios de splits qui se sont produits après cette date. Si Apple a fait un split 2:1 en 2014 et 4:1 en 2020, un prix de 1990 doit être divisé par 2 × 4 = 8.

Produit cumulatif de ratios — Formule SQL stable
CRAF(date) = EXP( SUM( LN(split_ratio) ) OVER (ORDER BY ex_date DESC ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) )
Équivalent à ∏(ratios après date) — EXP/LN évite les erreurs de floating point des produits itératifs

Pourquoi EXP(SUM(LN(x))) au lieu d'un simple produit ?

SQL n'a pas de fonction PRODUCT() analogue à SUM(). Une approche naïve avec une CTE récursive ou des self-joins est inefficace et sujette aux erreurs de floating point sur des longues séries (30+ ans). La transformation logarithmique convertit un produit en somme : ln(a × b × c) = ln(a) + ln(b) + ln(c). La fonction SUM() fenêtrée est native, optimisée, et numériquement stable. L'exponentiation inverse (EXP) reconvertit en produit. C'est la même technique utilisée pour calculer les rendements composés et les annualisations de volatilité.

SQL — Calcul du CRAF (Cumulative Ratio Adjustment Factor)
-- Table de référence des corporate actions
-- Ingérée par le tap Go de la Partie 2 depuis Yahoo Finance / Polygon.io
CREATE TABLE IF NOT EXISTS raw_zone.corporate_actions (
    id              BIGSERIAL PRIMARY KEY,
    symbol          VARCHAR(20) NOT NULL,
    isin            CHAR(12),
    ex_date         DATE        NOT NULL,    -- Date ex-action (premier jour SANS droit)
    record_date     DATE,                    -- Date d'enregistrement
    ca_type         VARCHAR(20) NOT NULL,    -- 'SPLIT', 'DIVIDEND', 'SPINOFF', 'MERGER'
    -- Pour les splits : ratio = nb nouvelles actions / nb anciennes actions
    -- Split 4:1 → ratio = 4.0 (le dénominateur d'ajustement du prix)
    split_ratio     NUMERIC(10,6),
    -- Pour les dividendes : montant en devise de l'action
    dividend_amount NUMERIC(14,6),
    dividend_currency CHAR(3),
    -- Commentaire éditorial
    description     TEXT,
    provider_name   VARCHAR(50)  NOT NULL,
    ingested_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index pour les jointures de fenêtre temporelle
CREATE INDEX idx_ca_symbol_date ON raw_zone.corporate_actions (symbol, ex_date DESC);
CREATE INDEX idx_ca_type        ON raw_zone.corporate_actions (ca_type, ex_date DESC);
dbt SQL — models/silver/fct_split_adjustment_factors.sql
{{ config(
    materialized='table',
    tags=['silver', 'corporate_actions']
) }}

WITH splits AS (
    SELECT
        symbol,
        ex_date,
        split_ratio,
        -- Validation : ratio doit être positif et non-nul
        CASE
            WHEN split_ratio IS NULL OR split_ratio <= 0 THEN NULL
            ELSE split_ratio
        END AS validated_ratio
    FROM {{ source('raw_zone', 'corporate_actions') }}
    WHERE ca_type = 'SPLIT'
      AND split_ratio IS NOT NULL
      AND split_ratio > 0
),

-- Calcul du facteur cumulatif pour chaque événement
-- La fenêtre DESC + UNBOUNDED FOLLOWING signifie :
--   "pour ce split ET tous les splits FUTURS" = ajustement rétroactif
cumulative_factors AS (
    SELECT
        symbol,
        ex_date,
        split_ratio,

        -- CRAF = produit de TOUS les ratios >= ex_date (splits futurs inclus)
        -- EXP(SUM(LN())) = produit cumulatif, numériquement stable
        EXP(SUM(LN(validated_ratio)) OVER (
            PARTITION BY symbol
            ORDER BY ex_date DESC
            ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        )) AS craf_from_this_event,

        -- Facteur d'ajustement AVANT cette date (pour ajuster les prix historiques)
        -- Pour les dates < ex_date, diviser par ce facteur
        EXP(SUM(LN(validated_ratio)) OVER (
            PARTITION BY symbol
            ORDER BY ex_date ASC
            ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        )) AS craf_for_historical_prices

    FROM splits
    WHERE validated_ratio IS NOT NULL
)

SELECT
    symbol,
    ex_date,
    split_ratio,
    craf_from_this_event,
    craf_for_historical_prices,
    -- Prix ajusté = prix brut / craf_for_historical_prices
    -- C'est ce facteur qui s'applique à toutes les barres AVANT ex_date
    ROUND(1.0 / craf_for_historical_prices, 8) AS price_adjustment_multiplier
FROM cumulative_factors
ORDER BY symbol, ex_date ASC

Maintenant, un modèle dbt peut joindre ce facteur aux barres Silver pour produire les prix ajustés :

SQL — Application du CRAF aux prix historiques (vue d'exemple)
-- Comment appliquer le facteur d'ajustement à une barre historique
-- Utilisé en couche Gold (voir section suivante)

WITH bars AS (
    SELECT symbol, trade_date, open_price, high_price, low_price,
           close_price, adj_close_price, volume
    FROM silver_zone.fct_market_data_daily
),

-- Pour chaque barre, trouver le facteur d'ajustement cumulatif
-- = produit de tous les splits qui se sont produits APRÈS trade_date
split_factors AS (
    SELECT
        b.symbol,
        b.trade_date,
        -- COALESCE(factor, 1.0) = si pas de split après cette date → facteur = 1
        COALESCE(
            EXP(SUM(LN(ca.split_ratio)) FILTER (WHERE ca.ex_date > b.trade_date)),
            1.0
        ) AS cumulative_split_factor
    FROM bars b
    LEFT JOIN raw_zone.corporate_actions ca
           ON ca.symbol = b.symbol
          AND ca.ca_type = 'SPLIT'
          AND ca.ex_date > b.trade_date
    GROUP BY b.symbol, b.trade_date
)

-- Prix ajusté = prix brut / facteur cumulatif des splits FUTURS
SELECT
    b.symbol,
    b.trade_date,
    b.close_price                                          AS close_unadjusted,
    ROUND(b.close_price / sf.cumulative_split_factor, 4)  AS close_split_adjusted,

    -- Yahoo Finance "Adj Close" inclut déjà les dividendes ET les splits
    -- Pour un backtester total return, utiliser adj_close_price directement
    b.adj_close_price                                      AS close_total_return_adjusted,

    -- Volumes ajustés en sens inverse : un split 4:1 × volume × 4
    ROUND(b.volume * sf.cumulative_split_factor)           AS volume_adjusted,

    sf.cumulative_split_factor

FROM bars b
JOIN split_factors sf USING (symbol, trade_date)
Dividendes

Ajustement des Dividendes : Total Return vs. Price Return

L'ajustement des dividendes est plus subtil que les splits. Le prix baisse le jour ex-dividende du montant du dividende, mais cela représente un transfert de richesse aux actionnaires — pas une perte. Un backtester qui ne réintègre pas les dividendes sous-estime systématiquement le rendement total, particulièrement sur les actions à haut rendement ou sur les longues périodes.

Facteur d'ajustement dividende (Dividend Adjustment Factor)
DAF(t) = (P_ex − D) / P_ex = 1 − (D / P_ex)
P_ex = prix de clôture le jour précédant ex-date | D = montant du dividende | DAF < 1 toujours

Les facteurs dividendes s'appliquent de la même façon que les splits : cumulativement sur toute la période précédant chaque ex-date.

dbt SQL — models/silver/fct_dividend_adjustment_factors.sql
{{ config(materialized='table', tags=['silver', 'corporate_actions']) }}

WITH dividends AS (
    SELECT
        ca.symbol,
        ca.ex_date,
        ca.dividend_amount,

        -- Prix de clôture la veille de l'ex-date (T-1)
        -- C'est le prix "cum-dividend" — avant que la bourse retire le dividende
        LAG(b.close_price, 0) OVER (
            PARTITION BY b.symbol
            ORDER BY b.trade_date
        ) AS close_price_day_before

    FROM {{ source('raw_zone', 'corporate_actions') }} ca
    JOIN {{ ref('fct_market_data_daily') }} b
      ON b.symbol = ca.symbol
      -- Clôture de la veille de l'ex-date
     AND b.trade_date = ca.ex_date - INTERVAL '1 day'
    WHERE ca.ca_type = 'DIVIDEND'
      AND ca.dividend_amount > 0
),

-- Dividend Adjustment Factor = (P_cum - D) / P_cum
daf_computed AS (
    SELECT
        symbol,
        ex_date,
        dividend_amount,
        close_price_day_before,
        CASE
            WHEN close_price_day_before > 0 THEN
                (close_price_day_before - dividend_amount) / close_price_day_before
            ELSE NULL
        END AS daf_single_event     -- Facteur pour CET événement uniquement
    FROM dividends
    WHERE close_price_day_before IS NOT NULL
      AND close_price_day_before > dividend_amount  -- sanity: dividende < prix
),

-- CRAF des dividendes : produit cumulatif de tous les DAF FUTURS
-- (même logique que pour les splits)
cumulative_daf AS (
    SELECT
        symbol,
        ex_date,
        dividend_amount,
        daf_single_event,

        EXP(SUM(LN(NULLIF(daf_single_event, 0))) OVER (
            PARTITION BY symbol
            ORDER BY ex_date ASC
            ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
        )) AS cumulative_daf

    FROM daf_computed
    WHERE daf_single_event IS NOT NULL
      AND daf_single_event > 0
      AND daf_single_event < 1       -- Doit être entre 0 et 1
)

SELECT
    symbol,
    ex_date,
    dividend_amount,
    ROUND(daf_single_event::NUMERIC, 8)  AS daf_single,
    ROUND(cumulative_daf::NUMERIC, 8)    AS cumulative_daf,
    -- Prix ajusté total return = prix brut × cumulative_daf
    -- (multiplier < 1 → ajuste les prix historiques à la baisse)
    cumulative_daf                       AS price_multiplier_for_history
FROM cumulative_daf
ORDER BY symbol, ex_date ASC

Yahoo Finance "Adj Close" : Total Return déjà calculé

Bonne nouvelle : Yahoo Finance calcule déjà Adj Close en intégrant à la fois les splits et les dividendes (Total Return). Si votre tap Yahoo ingère correctement le champ Adj Close (fait en Partie 2), vous avez les prix total return directement.

Quand calculer manuellement ? Quand vous utilisez des providers qui n'ajustent pas (BVC, certaines APIs crypto), ou quand vous voulez un contrôle granulaire de l'ajustement (ex: exclure les dividendes spéciaux, inclure uniquement les dividendes ordinaires).

Spin-offs & Fusions

Événements Complexes : Spin-offs et Fusions

Les spin-offs et fusions sont les corporate actions les plus complexes à gérer en base de données. Elles impliquent la création ou la disparition d'identifiants de titres — ce qui remet en cause notre stratégie ISIN/ticker.

Spin-off : IBM scinde Kyndryl (2021)

Le 3 novembre 2021, IBM (ISIN US4592001014) a scindé sa division Infrastructure Services en une société indépendante : Kyndryl Holdings (ISIN US50155Q1004). Chaque actionnaire IBM a reçu 1 action KD pour 5 actions IBM. Le prix IBM a baissé d'environ $134 → $120 à l'ex-date.

1
Créer le nouvel instrument
Kyndryl (KD) est inséré dans dim_instruments avec valid_from = ex_date, ISIN = US50155Q1004.
2
Calculer le ratio
Valeur du spin-off / valeur IBM avant ex-date. Facteur IBM = (120/134) ≈ 0.896.
3
Ajuster IBM historique
Multiplier tous les prix IBM avant ex-date par le facteur spin-off ≈ 0.896 pour continuité.
4
Démarrer KD à ex-date
La série de prix KD commence à l'ex-date. Pas d'historique avant. is_tradable passe de FALSE à TRUE.
SQL — Enregistrement d'un spin-off dans la table corporate_actions
-- Enregistrement du spin-off IBM → Kyndryl
INSERT INTO raw_zone.corporate_actions (
    symbol, isin, ex_date, record_date, ca_type,
    split_ratio, description, provider_name
) VALUES
-- L'événement affecte IBM (action mère)
(
    'IBM',
    'US4592001014',
    '2021-11-03',   -- ex-date (premier jour ex-spin-off)
    '2021-11-01',
    'SPINOFF',
    0.8955,         -- facteur d'ajustement : (120.05 / 134.04) = 0.8956
    'IBM spins off infrastructure services as Kyndryl Holdings (KD). '
    'Ratio: 1 KD share per 5 IBM shares. '
    'IBM price adjustment factor: ~0.896',
    'MANUAL_REVIEW'
),
-- Naissance du nouvel instrument Kyndryl
-- (géré via le snapshot SCD2 de dim_instruments en Partie 3)
(
    'KD',
    'US50155Q1004',
    '2021-11-03',
    '2021-11-01',
    'SPINOFF_NEW',  -- Type custom : nouvel instrument issu d'un spin-off
    NULL,
    'Kyndryl Holdings first trading day. Spun off from IBM. '
    'Opening price ~$28.10. is_tradable changes from FALSE to TRUE.',
    'MANUAL_REVIEW'
);

Fusion : Gestion du Ticker Survivant

Pour une fusion tout-cash (ex: Microsoft acquiert Activision Blizzard — ATVI), la procédure est plus simple : la série de prix ATVI se termine à la date de clôture de la transaction. Le ticker ATVI est marqué is_tradable = FALSE via le mécanisme SCD2 de la Partie 3.

SQL — Clôture d'une série après fusion tout-cash
-- Fusion Microsoft / Activision Blizzard — clôture ATVI (octobre 2023)
INSERT INTO raw_zone.corporate_actions (
    symbol, isin, ex_date, ca_type, dividend_amount, description, provider_name
) VALUES (
    'ATVI',
    'US00507V1098',
    '2023-10-13',   -- Dernier jour de trading ATVI
    'MERGER',
    95.00,          -- Rachat tout-cash à $95/action par Microsoft
    'Microsoft acquires Activision Blizzard in all-cash deal. '
    'ATVI shareholders receive $95.00/share in cash. '
    'ATVI delisted after market close. Last price: $94.98.',
    'MANUAL_REVIEW'
);

-- Le tap Meltano détectera que ATVI disparaît de l'API Yahoo
-- Le snapshot dbt (Partie 3) avec invalidate_hard_deletes=true
-- fermera automatiquement la ligne :
--   dbt_valid_to = 2023-10-13
--   is_tradable  = FALSE (propagé depuis la source)
Intégration dans la Couche Gold

Intégrer les Ajustements dans le Mart Gold

Toutes les tables de facteurs calculées en Silver sont maintenant combinées dans le mart Gold. Le mart expose directement les prix ajustés — les utilisateurs en aval (backtester, agent IA, dashboard) n'ont jamais à gérer les facteurs eux-mêmes.

dbt SQL — models/gold/mart_adjusted_prices.sql
{{ config(
    materialized='table',
    tags=['gold', 'corporate_actions', 'ai_marts'],
    post_hook="ANALYZE {{ this }}"
) }}

WITH bars AS (
    SELECT
        symbol, trade_date,
        open_price, high_price, low_price, close_price,
        adj_close_price,   -- Yahoo Total Return (splits + dividendes)
        volume
    FROM {{ ref('fct_market_data_daily') }}
),

-- Facteurs de splits par (symbol, trade_date)
-- Pour chaque barre, on veut le produit des splits APRÈS cette date
split_factors_by_bar AS (
    SELECT
        b.symbol,
        b.trade_date,
        COALESCE(
            EXP(
                SUM(LN(ca.split_ratio))
                FILTER (WHERE ca.ex_date > b.trade_date AND ca.split_ratio > 0)
            ),
            1.0
        ) AS cumulative_split_factor

    FROM bars b
    LEFT JOIN {{ source('raw_zone', 'corporate_actions') }} ca
           ON ca.symbol = b.symbol
          AND ca.ca_type = 'SPLIT'
    GROUP BY b.symbol, b.trade_date
),

-- Facteurs de dividendes par (symbol, trade_date)
div_factors_by_bar AS (
    SELECT
        b.symbol,
        b.trade_date,
        COALESCE(
            EXP(
                SUM(LN(NULLIF(daf.cumulative_daf, 0)))
                FILTER (WHERE daf.ex_date > b.trade_date)
            ),
            1.0
        ) AS cumulative_div_factor

    FROM bars b
    LEFT JOIN {{ ref('fct_dividend_adjustment_factors') }} daf
           ON daf.symbol = b.symbol
    GROUP BY b.symbol, b.trade_date
)

SELECT
    b.symbol,
    b.trade_date,

    -- ═══ Prix BRUTS ═══
    b.open_price                                                AS open_raw,
    b.high_price                                                AS high_raw,
    b.low_price                                                 AS low_raw,
    b.close_price                                               AS close_raw,
    b.volume                                                    AS volume_raw,

    -- ═══ Ajustement splits uniquement ═══
    ROUND(b.open_price  / sf.cumulative_split_factor, 4)       AS open_split_adj,
    ROUND(b.high_price  / sf.cumulative_split_factor, 4)       AS high_split_adj,
    ROUND(b.low_price   / sf.cumulative_split_factor, 4)       AS low_split_adj,
    ROUND(b.close_price / sf.cumulative_split_factor, 4)       AS close_split_adj,
    ROUND(b.volume      * sf.cumulative_split_factor)          AS volume_split_adj,

    -- ═══ Ajustement Total Return (splits + dividendes) ═══
    -- Option A : Yahoo Adj Close (recommandé si source Yahoo)
    b.adj_close_price                                          AS close_total_return_yahoo,

    -- Option B : Calcul manuel (pour providers sans Adj Close)
    ROUND(b.close_price / sf.cumulative_split_factor
                        * df.cumulative_div_factor, 4)         AS close_total_return_manual,

    -- ═══ Métadonnées d'ajustement ═══
    sf.cumulative_split_factor,
    df.cumulative_div_factor,
    ROUND((sf.cumulative_split_factor * (1 / NULLIF(df.cumulative_div_factor, 0)))::NUMERIC, 6)
                                                               AS combined_adjustment_factor

FROM bars b
JOIN split_factors_by_bar sf USING (symbol, trade_date)
JOIN div_factors_by_bar   df USING (symbol, trade_date)

Attention : Ne jamais mélanger prix ajustés et bruts dans le même backtest

Un backtest qui calcule le signal sur close_total_return_adj mais soumet les ordres sur close_raw aura un historique de performance irréel. Utilisez soit les prix ajustés pour les signaux et les ordres simulés, soit gardez les bruts partout. La convention institutionnelle : signaux sur Total Return Adj, ordres live sur Raw (le prix réel du marché).

Vérification de l'Ajustement

SQL — Vérification des ajustements AAPL autour du split août 2020
-- Validation : vérifier que les prix ajustés sont continus autour du split 4:1 AAPL
-- Le prix ajusté ne devrait pas avoir de "saut" le 31 août 2020

SELECT
    trade_date,
    close_raw,
    close_split_adj,
    -- Rendement journalier sur prix bruts (INCORRECT : montre le crash fantôme)
    ROUND(
        (close_raw - LAG(close_raw) OVER (ORDER BY trade_date)) /
        NULLIF(LAG(close_raw) OVER (ORDER BY trade_date), 0) * 100,
        2
    ) AS daily_return_raw_pct,
    -- Rendement journalier sur prix ajustés (CORRECT : continuité maintenue)
    ROUND(
        (close_split_adj - LAG(close_split_adj) OVER (ORDER BY trade_date)) /
        NULLIF(LAG(close_split_adj) OVER (ORDER BY trade_date), 0) * 100,
        2
    ) AS daily_return_adj_pct,
    cumulative_split_factor

FROM gold_zone.mart_adjusted_prices
WHERE symbol = 'AAPL'
  AND trade_date BETWEEN '2020-08-25' AND '2020-09-04'
ORDER BY trade_date;

-- Résultat attendu :
-- trade_date | close_raw | close_split_adj | daily_return_raw_pct | daily_return_adj_pct
-- 2020-08-28 |   499.23  |    124.81       | +1.2%                | +1.2%   ← normal
-- 2020-08-31 |   129.04  |    129.04       | -74.2% ← CRASH FANTÔME | +3.4% ← CORRECT
-- 2020-09-01 |   134.18  |    134.18       | +3.9%                | +3.9%   ← normal

-- Si daily_return_adj_pct montre aussi -74% : le facteur n'est pas appliqué correctement.
Facteurs d'ajustement cumulatifs — Apple (AAPL) sur 15 ans
Sources de Données

Sources de Données pour les Corporate Actions

La qualité de vos ajustements dépend directement de la qualité de votre source de corporate actions. Un split mal daté génère des barres mal ajustées sur des années entières.

Source Coût Splits Dividendes Spin-offs Fiabilité
Yahoo Finance API Gratuit ✅ Adj Close auto ✅ Adj Close auto ⚠️ Partiel Bonne pour US
Polygon.io $29+/mois ✅ Endpoint dédié ✅ Endpoint dédié ✅ Bon coverage Très bonne
OpenFIGI / OPENFIGI API Gratuit ❌ Non ❌ Non ✅ Mapping ISIN Excellente (ISIN)
Refinitiv / LSEG $1000+/mois ✅ Complet ✅ Complet ✅ Complet Institutionnelle
QuantLib + manuel Gratuit + temps ✅ Manuel ✅ Manuel ✅ Manuel Dépend de vous
BVC (Casablanca) Gratuit ⚠️ Sparse ✅ Corporate actions publiées ⚠️ Rare Bonne pour MAD
yaml — Tap Meltano pour Polygon.io Corporate Actions
plugins:
  extractors:
    - name: tap-polygon-corporate-actions
      namespace: tap_polygon_corporate_actions
      pip_url: tap-polygon
      config:
        api_key: $POLYGON_API_KEY
        start_date: "1990-01-01"
        tickers_list: "{{ env_var('SECURITIES_UNIVERSE') }}"   # Liste des tickers à surveiller

      streams:
        - name: stock_splits
          endpoint: v3/reference/splits
          # Polygon retourne : ticker, ex_date, split_from, split_to
          # ratio = split_to / split_from (ex: 4:1 → ratio=4)

        - name: dividends
          endpoint: v3/reference/dividends
          # Polygon retourne : ticker, ex_dividend_date, cash_amount, declaration_date

      select:
        - stock_splits.*
        - dividends.*

Points Clés de la Partie 4

Le Securities Master est maintenant mathématiquement correct. Les modèles IA peuvent calculer des indicateurs continus sur 30 ans sans artefacts. Dans la Partie 5, nous exposons ces données via un serveur MCP et des dashboards Evidence.dev.

🤖

Partie 5 — Interface IA : MCP & Dashboards Evidence.dev

Exposer le Securities Master aux LLMs via Model Context Protocol et construire des dashboards BI-as-code.

Auto-Évaluation

Quiz : Corporate Actions & Ajustements

Cliquez sur chaque question pour révéler la réponse.

Pourquoi un split 4-pour-1 génère-t-il un faux signal de vente dans un algorithme de momentum ?
Le prix nominal chute de ~75% sans destruction de valeur réelle. Avant le split, Apple clôture à $499. Après le split 4:1, chaque actionnaire détient 4× plus d'actions valant $125 chacune. Valeur totale : identique. Mais un algorithme qui calcule le rendement 1 jour : (125 − 499)/499 = −74.5% interprétera cela comme un crash catastrophique et générera un signal SELL/SHORT massif. Sans ajustement des prix historiques, la continuité mathématique est brisée et tous les indicateurs techniques (RSI, MACD, SMA, Bollinger Bands) sont corrompus pour toute la période précédant le split.
Expliquez mathématiquement pourquoi EXP(SUM(LN(x))) est équivalent à un produit cumulatif.
Propriété fondamentale des logarithmes : ln(a × b × c) = ln(a) + ln(b) + ln(c). Donc : a × b × c = EXP(ln(a) + ln(b) + ln(c)) = EXP(SUM des LN). SQL ne dispose pas d'une fonction PRODUCT() fenêtrée native, mais possède SUM() fenêtrée. La transformation logarithmique convertit le produit en somme — calculable par SUM(). L'exponentiation inverse reconvertit. En pratique : si Apple a eu un split 2:1 en 2014 (ratio=2) et 4:1 en 2020 (ratio=4), le CRAF pour 2010 = EXP(LN(2) + LN(4)) = EXP(LN(8)) = 8. Prix 2010 ajusté = prix_brut / 8.
Quelle est la différence entre "Price Return Adjusted" et "Total Return Adjusted" pour un backtester ?
Price Return Adjusted inclut uniquement les splits. Total Return Adjusted inclut splits ET dividendes réinvestis. Pour un backtester de momentum sur actions à faibles dividendes (tech), la différence est minime sur 1-2 ans. Sur des actions à haut dividende (utilities, REITs) ou sur 20+ ans, la différence est massive : les dividendes représentent 40-60% du rendement total historique du S&P 500. Un backtest qui ignore les dividendes sous-estime systématiquement la performance des stratégies buy-and-hold et des stratégies de revenus. Yahoo Finance "Adj Close" intègre les deux (Total Return). Pour construire un indice ou un benchmark cohérent, toujours utiliser Total Return.
Comment le Securities Master gère-t-il un spin-off qui crée un nouveau ticker ?
En combinant trois mécanismes : (1) La table corporate_actions enregistre l'événement spin-off avec le facteur d'ajustement de l'action mère. (2) Le nouveau ticker (ex: KD pour Kyndryl) est inséré dans la source Bronze avec ingested_at = ex_date. Le snapshot dbt de la Partie 3 crée automatiquement une nouvelle ligne dans dim_instruments_snapshot avec dbt_valid_from = ex_date et is_tradable = TRUE. (3) L'action mère (IBM) voit ses prix historiques multipliés par le facteur spin-off dans le mart Gold. La série de prix KD commence à ex-date sans historique antérieur — ce qui est mathématiquement correct.
Dans quel sens le volume est-il ajusté lors d'un split, et pourquoi ?
En sens inverse du prix : volume_ajusté = volume_brut × facteur_split. Un split 4:1 multiplie le nombre d'actions en circulation par 4. Pour maintenir la continuité des indicateurs de liquidité (ADV, volume $ = volume × prix), le volume historique doit aussi être multiplié par 4. Ainsi : volume_ajusté × close_split_adj = volume_brut × close_brut = dollar volume constant. Si on n'ajustait pas le volume, le Dollar Volume semblerait avoir chuté de 75% au moment du split — ce qui donnerait l'illusion d'une forte baisse de liquidité.
Pourquoi le Dividend Adjustment Factor (DAF) est-il toujours strictement inférieur à 1 ?
Parce que le prix ex-dividende est toujours inférieur au prix cum-dividende. La formule DAF = (P_ex − D) / P_ex = 1 − (D/P_ex). Comme D > 0 et P_ex > 0, DAF est toujours < 1. Physiquement : le dividende est un transfert de richesse de la société aux actionnaires. La valeur boursière de la société diminue du montant distribué à l'ex-date. Pour rendre l'historique continu (comme si le dividende avait été réinvesti dans l'action), les prix antérieurs à l'ex-date sont multipliés par DAF — les ramenant à la baisse pour correspondre au niveau post-dividende. Un DAF = 0.99 pour un dividende de 1% est typique d'une action à dividende modéré.
Securities Master 4/6