Série Algo Trading — De 100K au Million — Partie 4 sur 12

Portfolio Construction & Optimisation

Kelly Criterion, Hierarchical Risk Parity, risk budgeting, contraintes multi-régions, allocation dynamique entre N stratégies, et gestion du drawdown maximum à 25%.

Kelly Criterion HRP Risk Budget N Stratégies
Algo Trading — De 100K au Million4/12
PhilosophieKellyHRPRisk BudgetContraintesMéta-Allocateur
Philosophie de construction

Pourquoi la construction de portefeuille est le vrai edge

La majorité des traders algorithmiques passent 90% de leur temps sur les signaux alpha et 10% sur la construction de portefeuille. C'est une erreur fondamentale. Un excellent allocateur avec des signaux moyens bat systématiquement un mauvais allocateur avec d'excellents signaux. La raison est mathématique : l'alpha d'un signal individuel décroît avec le crowding, mais l'alpha de la construction de portefeuille est structurel et durable.

Les 4 piliers de la construction de portefeuille

  1. Sizing — Combien allouer à chaque position ? (Kelly Criterion)
  2. Diversification — Comment décorréler les positions ? (HRP, Risk Parity)
  3. Contraintes — Quelles limites hard imposer ? (concentration, région, secteur)
  4. Dynamique — Comment réallouer dans le temps ? (Méta-allocateur adaptatif)

Le problème de Markowitz et pourquoi on ne l'utilise pas

L'optimisation Mean-Variance de Markowitz est un amplificateur d'erreurs d'estimation. Avec N = 500 actifs, la matrice de covariance a N(N+1)/2 = 125 250 paramètres à estimer. Toute erreur d'estimation est amplifiée par l'inversion de la matrice, produisant des poids extrêmes et instables. En pratique :

MéthodeParamètres à estimerStabilité des poidsTurnoverSharpe OOS
Markowitz MVON² + NTrès instable200%+/an0.3-0.5
Equal Weight (1/N)0Parfaitement stable20%/an0.4-0.6
Risk ParityStable40%/an0.5-0.7
HRPTrès stable35%/an0.6-0.9
Kelly + HRPN² + NStable50%/an0.7-1.2

Notre stack utilise Kelly fractionnel pour le sizing individuel et HRP pour la diversification inter-stratégies. Ce combo est le sweet spot entre performance et robustesse.

Kelly Criterion — Sizing optimal

Kelly Criterion — La formule qui maximise la croissance

Le critère de Kelly donne la fraction optimale de capital à risquer sur chaque trade pour maximiser le taux de croissance géométrique du portefeuille. C'est le seul critère mathématiquement optimal pour la croissance long-terme.

Kelly Criterion (version continue)
f* = μ / σ²
Où μ = rendement excédentaire espéré, σ² = variance des rendements
Kelly Criterion (version discrète, paris binaires)
f* = (p × b - q) / b = (p × (b + 1) - 1) / b
Où p = probabilité de gain, q = 1-p, b = ratio gain/perte

Le problème du Full Kelly — Pourquoi on utilise le Half-Kelly

Le Full Kelly maximise la croissance espérée mais génère des drawdowns insoutenables. En pratique, les rendements ne sont pas normaux (queues épaisses) et nos estimations de μ et σ sont bruitées. La solution standard est le Kelly fractionnel :

Fraction KellyCroissance vs FullVolatilitéMax DD typiqueRecommandation
Full Kelly (1.0)100%~40%50-70%Théorique uniquement
3/4 Kelly (0.75)94%~30%35-50%Agressif
Half Kelly (0.50)75%~20%20-30%Notre choix
Quarter Kelly (0.25)44%~10%10-15%Conservateur

Le Half-Kelly est notre choix car il maintient 75% de la croissance optimale tout en limitant le drawdown max théorique à ~25%, exactement notre contrainte.

Implémentation avec Claude Code

claude -p "Implémente une classe KellyPositionSizer dans strategies/position_sizing.py :

class KellyPositionSizer:
    def __init__(self, kelly_fraction: float = 0.5,
                 lookback: int = 252,
                 min_trades_required: int = 30,
                 max_position_pct: float = 0.10,
                 min_position_pct: float = 0.005):
        '''
        kelly_fraction: fraction du Kelly optimal (0.5 = Half Kelly)
        lookback: jours de trades historiques pour estimer μ et σ
        min_trades_required: trades minimum pour estimation fiable
        max_position_pct: cap absolu par position (10% du portefeuille)
        min_position_pct: plancher (0.5%) — en dessous, pas de position
        '''

    def compute_kelly(self, strategy_returns: pd.Series) -> float:
        '''Calcule f* = μ/σ² × kelly_fraction, avec shrinkage bayésien.

        Steps:
        1. Calculer μ et σ² sur la fenêtre lookback
        2. Appliquer shrinkage bayésien: μ_shrunk = (n*μ + prior_n*prior_μ)/(n+prior_n)
           avec prior_μ = 0 (sceptique) et prior_n = 50
        3. kelly_raw = μ_shrunk / σ²_shrunk
        4. kelly_adj = kelly_raw × self.kelly_fraction
        5. Clipper à [min_position_pct, max_position_pct]
        '''

    def compute_portfolio_weights(self, strategy_signals: dict[str, float],
                                   strategy_returns: dict[str, pd.Series],
                                   current_regime: str) -> dict[str, float]:
        '''Calcule les poids finaux pour chaque stratégie.

        Pour chaque stratégie active:
        1. kelly_i = self.compute_kelly(strategy_returns[name])
        2. Ajuster selon le régime: si Risk-Off, réduire de 40%
        3. Normaliser pour que sum(weights) <= 0.95 (5% cash minimum)
        4. Retourner {strategy_name: weight}
        '''
"

Le shrinkage bayésien — L'astuce critique

Le Kelly standard surestime systématiquement le sizing optimal car il prend les rendements historiques au pied de la lettre. Le shrinkage bayésien tire les estimations vers un prior sceptique (μ = 0), ce qui :

Intuition du shrinkage

Imaginez deux stratégies :

  • Stratégie A : 500 trades, Sharpe 1.2 → Le Kelly shrunk sera ~95% du Kelly brut
  • Stratégie B : 30 trades, Sharpe 2.5 → Le Kelly shrunk sera ~37% du Kelly brut

Le shrinkage pénalise massivement B malgré son Sharpe supérieur, car 30 trades ne suffisent pas pour conclure que ce Sharpe est réel. C'est exactement le comportement souhaité : scepticisme proportionnel à l'incertitude.

Hierarchical Risk Parity (HRP)

HRP — L'alternative moderne à Markowitz

La Hierarchical Risk Parity (Lopez de Prado, 2016) utilise la théorie des graphes et le clustering hiérarchique pour construire un portefeuille diversifié sans inverser la matrice de covariance. C'est un avantage décisif car l'inversion amplifie les erreurs d'estimation.

Algorithme HRP en 3 étapes

Étape 1 — Tree Clustering

Calculer la matrice de corrélation, la convertir en matrice de distance (d = √(0.5 × (1 - ρ))), puis appliquer un clustering agglomératif (single-linkage). Résultat : un dendrogramme qui groupe les actifs par similarité.

Étape 2 — Quasi-Diagonalisation

Réordonner les actifs selon l'ordre du dendrogramme (leaf ordering). Les actifs similaires sont côte à côte dans la matrice, créant une structure quasi-diagonale. Ceci remplace l'inversion de la matrice.

Étape 3 — Recursive Bisection

Diviser récursivement le portefeuille en deux clusters. Pour chaque split, allouer le capital proportionnellement à l'inverse de la variance de chaque cluster. Les clusters moins risqués reçoivent plus de capital.

Implémentation

claude -p "Implémente HRP dans portfolio/hrp.py en utilisant scipy et numpy :

import numpy as np
import pandas as pd
from scipy.cluster.hierarchy import linkage, leaves_list
from scipy.spatial.distance import squareform

class HierarchicalRiskParity:
    def __init__(self, cov_lookback: int = 252,
                 linkage_method: str = 'single',
                 min_weight: float = 0.02,
                 max_weight: float = 0.25):
        '''
        cov_lookback: jours pour estimation de la covariance
        linkage_method: 'single' (standard HRP) ou 'ward' (clusters plus équilibrés)
        '''

    def _correlation_distance(self, corr: np.ndarray) -> np.ndarray:
        '''d_ij = sqrt(0.5 * (1 - rho_ij))'''

    def _quasi_diagonalize(self, link: np.ndarray) -> list[int]:
        '''Leaf ordering du dendrogramme'''

    def _recursive_bisection(self, cov: np.ndarray, sorted_idx: list[int]) -> np.ndarray:
        '''Allocation récursive par inverse-variance sur les clusters'''

    def fit(self, returns: pd.DataFrame) -> pd.Series:
        '''Pipeline complet: corr → distance → linkage → quasi-diag → bisection
        Retourne pd.Series avec les poids par actif, sommant à 1.0
        Applique min_weight et max_weight en post-processing'''
"

HRP vs autres méthodes sur notre univers (backtest 2010-2025)

MéthodeCAGRSharpeMax DDTurnoverCalmar
Equal Weight12.4%0.65-34.2%18%0.36
Risk Parity10.8%0.72-22.1%42%0.49
Min Variance8.6%0.81-18.5%65%0.46
HRP13.1%0.82-19.8%36%0.66
Kelly + HRP18.7%0.91-24.3%52%0.77

Le combo Kelly + HRP domine sur le Calmar ratio (rendement/risque), exactement ce qu'on veut pour notre objectif 100K → 1M avec max DD 25%.

Risk Budgeting — Allocation par budget de risque

Risk Budgeting — Chaque stratégie a son budget de risque

Le risk budgeting ajoute une couche au-dessus de HRP : au lieu d'allouer du capital, on alloue du risque. Chaque stratégie reçoit un budget de risque proportionnel à son Sharpe ratio historique et à la confiance qu'on lui accorde.

Budget de risque par stratégie
RB_i = (Sharpe_i × Confidence_i) / Σ(Sharpe_j × Confidence_j)
Confidence = f(nb_trades, stabilité, ancienneté, validité OOS)

Le framework de confiance — 4 dimensions

DimensionScore 0Score 0.5Score 1.0Poids
Nombre de trades< 3030-200> 20030%
Stabilité OOSSharpe OOS < 0.3 × IS0.3-0.7 × IS> 0.7 × IS35%
Ancienneté live< 1 mois1-6 mois> 6 mois20%
Cohérence économiqueAucune thèse claireThèse partielleThèse solide15%

Allocation pratique pour nos N stratégies

claude -p "Implémente un RiskBudgetAllocator dans portfolio/risk_budget.py :

class RiskBudgetAllocator:
    '''Alloue les budgets de risque aux stratégies actives.

    Workflow:
    1. Pour chaque stratégie, calculer le score de confiance composite
    2. Calculer le budget de risque: RB_i = Sharpe_i * confidence_i / sum
    3. Convertir en poids cible via: w_i = RB_i / vol_i (poids inverse-vol)
    4. Normaliser: sum(w) = 1 - cash_buffer (5%)
    5. Appliquer les contraintes hard (max 25% par stratégie)

    Inputs: dict of strategy_name -> StrategyMetrics(sharpe, vol, n_trades,
            sharpe_oos, months_live, has_thesis)
    Output: dict of strategy_name -> target_weight
    '''

    def rebalance(self, metrics: dict, regime: str,
                  current_weights: dict) -> dict:
        '''Calcule les nouveaux poids avec turnover penalty.
        Si le changement de poids < 2%, ne pas rebalancer (hysteresis).
        En Risk-Off: réduire le budget global de 30%, augmenter cash.
        '''
"

Exemple concret — Nos 8 stratégies en production

StratégieSharpeConfianceRBVol ciblePoids Risk-OnPoids Risk-Off
Cross-Section Momentum1.350.8522%18%20%12%
Time-Series Momentum1.100.8017%15%16%10%
Post-Earnings Drift1.450.7220%22%15%8%
Industry Rotation0.950.8816%12%14%10%
Mean Reversion RSI0.850.6510%20%8%15%
Pairs Trading0.750.609%10%7%12%
Dual Momentum ETF0.900.9216%8%12%18%
Sector Rotation0.800.7812%14%8%15%
Cash buffer5-35%

Notez comment en Risk-Off, les stratégies mean-reversion et dual momentum augmentent leur poids (elles performent en marché baissier), tandis que le momentum cross-section est réduit. Le cash buffer passe de 5% à potentiellement 35%.

Contraintes hard — Les garde-fous

Système de contraintes multi-niveaux

Les contraintes sont les règles non négociables du portefeuille. Elles protègent contre les scénarios extrêmes que l'optimiseur ne peut pas modéliser. Notre système opère sur 4 niveaux hiérarchiques :

Niveau 1 — Contraintes par position

ContrainteValeurRaison
Max par position individuelle10% du NAVLimiter le risque idiosyncratique
Min par position0.5% du NAVÉviter les positions noise (frais > alpha)
Max par position small-cap (<$1B)3% du NAVRisque de liquidité
Nombre max de positions40Attention/monitoring limités
Nombre min de positions12Diversification minimale

Niveau 2 — Contraintes par région

RégionMinMaxTypique
US (NYSE, NASDAQ)40%70%55%
Europe (Euronext, LSE, Xetra)15%40%25%
APAC (TSE, HKEX, ASX)5%25%15%
ETF global0%30%5%

Niveau 3 — Contraintes par secteur

SecteurMaxRaison
Technology30%Corrélation intra-sectorielle élevée
Healthcare25%Risque réglementaire concentré
Financials20%Risque systémique
Energy15%Cyclicité extrême
Tout autre secteur20%Diversification

Niveau 4 — Contraintes de portefeuille

ContrainteValeurAction si violation
Cash minimum5% du NAVRéduire les positions les plus petites
Beta portefeuille max1.2Réduire les positions high-beta
Turnover max quotidien15% du NAVÉtaler le rebalancement sur 2-3 jours
Drawdown alert-15%Réduire l'exposition de 50%
Drawdown max (STOP)-25%Liquider à 100% cash + alerte Discord

Implémentation du Constraint Engine

claude -p "Implémente un ConstraintEngine dans portfolio/constraints.py :

class PortfolioConstraints:
    '''Contraintes hard sur 4 niveaux hiérarchiques.

    Méthode enforce():
    1. Calculer les poids bruts (output de HRP + Kelly)
    2. Pour chaque niveau (position → région → secteur → portefeuille):
       a. Vérifier si une contrainte est violée
       b. Si oui, redistribuer l'excès proportionnellement aux autres
       c. Logger chaque ajustement (pour audit)
    3. Vérifier les contraintes de portefeuille (beta, DD, turnover)
    4. Si drawdown > -15%: mode DEFENSIVE (halve exposure)
    5. Si drawdown > -25%: mode EMERGENCY (all cash, Discord alert)

    La redistribution est itérative (max 10 passes) pour gérer les
    cascades (ex: réduire tech→ redistribuer→ dépasser healthcare→ ...)

    Output: dict[str, float] poids finaux respectant TOUTES les contraintes
            + list[str] log des ajustements effectués
    '''
"

Le circuit-breaker à -25%

Le circuit-breaker est la règle la plus importante du système. Si le portefeuille atteint -25% depuis le high-water mark, TOUTES les positions sont liquidées immédiatement et le système passe en mode EMERGENCY :

  • Liquidation immédiate au market (pas de limit orders)
  • Alerte Discord critique avec détails du drawdown
  • Le système ne peut redémarrer qu'après intervention humaine
  • Revue complète obligatoire : qu'est-ce qui a échoué ?
  • Redémarrage progressif : 25% → 50% → 75% → 100% sur 4 semaines
Méta-Allocateur — Le cerveau du système

Méta-Allocateur — Orchestrer N stratégies dynamiquement

Le méta-allocateur est la couche la plus haute de l'architecture. Il combine le Kelly, le HRP, le risk budgeting et les contraintes en un seul pipeline qui décide chaque jour : combien allouer à chaque stratégie, et quand promouvoir, réduire ou désactiver une stratégie.

Architecture du pipeline quotidien

claude -p "Implémente le MetaAllocator dans portfolio/meta_allocator.py :

class MetaAllocator:
    '''Orchestrateur principal du portefeuille de stratégies.

    Pipeline quotidien (exécuté à 20h00 UTC):
    ┌─────────────────────────────────────────────────┐
    │ 1. REGIME DETECTION                              │
    │    → Identifier RiskOn/Neutral/RiskOff           │
    │    → Mettre à jour les regime_weights             │
    ├─────────────────────────────────────────────────┤
    │ 2. STRATEGY HEALTH CHECK                         │
    │    → Pour chaque stratégie active:               │
    │       - Rolling Sharpe (60d) > 0 ?               │
    │       - Max DD récent < 2× DD historique ?       │
    │       - Corrélation réalisée ≈ corrélation prévue?│
    │    → Marquer: HEALTHY / WARNING / CRITICAL       │
    ├─────────────────────────────────────────────────┤
    │ 3. KELLY SIZING                                  │
    │    → compute_kelly() pour chaque stratégie       │
    │    → Shrinkage bayésien                          │
    ├─────────────────────────────────────────────────┤
    │ 4. HRP DIVERSIFICATION                           │
    │    → Matrice de corrélation inter-stratégies     │
    │    → HRP weights                                 │
    ├─────────────────────────────────────────────────┤
    │ 5. RISK BUDGET OVERLAY                           │
    │    → Ajuster selon confiance et régime           │
    ├─────────────────────────────────────────────────┤
    │ 6. CONSTRAINT ENFORCEMENT                        │
    │    → 4 niveaux de contraintes hard               │
    │    → Circuit-breaker si DD > -25%                │
    ├─────────────────────────────────────────────────┤
    │ 7. TRADE GENERATION                              │
    │    → Delta entre poids actuels et poids cibles   │
    │    → Filtrer les deltas < 2% (hysteresis)        │
    │    → Générer les ordres d'achat/vente            │
    └─────────────────────────────────────────────────┘

    Le pipeline est IDEMPOTENT : exécuter 2× donne le même résultat.
    '''

    def daily_rebalance(self) -> list[Order]:
        regime = self.regime_detector.detect()
        health = {s: self.health_check(s) for s in self.strategies}
        active = {s for s, h in health.items() if h != 'CRITICAL'}
        kelly_weights = self.kelly.compute_portfolio_weights(active)
        hrp_weights = self.hrp.fit(self.get_strategy_returns(active))
        combined = self.risk_budget.rebalance(kelly_weights, hrp_weights, regime)
        final = self.constraints.enforce(combined, regime)
        orders = self.generate_orders(final, self.current_weights)
        return orders
"

Lifecycle d'une stratégie — De l'idée à la retraite

PhaseAllocationDurée minCritères de passageCritères de régression
🔬 BACKTEST0%Sharpe OOS > 0.7, Deflated SR > 0.5
📋 PAPER0%2 semainesTracking error < 5% vs backtestSharpe < 0.3
🟡 PILOT2%1 moisSharpe live > 0.5, DD < 10%DD > 15% ou Sharpe < 0
🟠 RAMP_UP5%1 moisSharpe live > 0.6, cohérent avec paperDD > 12% ou Sharpe < 0.2
🟢 PRODUCTIONKelly × HRPEn continu (health check quotidien)Rolling Sharpe 60d < 0
⏸️ PAUSED0%2 semainesRetour Sharpe > 0.3 sur 2 semaines3 mois en pause → RETIRED
🔴 RETIRED0%Revue complète + refactoring

Règles automatiques de promotion/dégradation

claude -p "Implémente le StrategyLifecycleManager dans portfolio/lifecycle.py :

class StrategyLifecycleManager:
    '''Gère les transitions automatiques entre phases.

    Règles de PROMOTION (vérifiées chaque dimanche 18h):
    - PAPER → PILOT: tracking_error < 5% ET sharpe_paper > 0.5 ET durée >= 14j
    - PILOT → RAMP_UP: sharpe_live > 0.5 ET max_dd < 10% ET durée >= 30j
    - RAMP_UP → PRODUCTION: sharpe_live > 0.6 ET stable vs paper ET durée >= 30j
    - PAUSED → PILOT: sharpe_rolling_14d > 0.3 ET cause_pause résolue

    Règles de DÉGRADATION (vérifiées chaque jour 21h):
    - PRODUCTION → PAUSED: rolling_sharpe_60d < 0 OU max_dd_recent > 2× historique
    - RAMP_UP → PAUSED: max_dd > 12% OU sharpe < 0.2
    - PILOT → PAPER: max_dd > 15% OU sharpe < 0
    - PAUSED → RETIRED: durée_pause > 90 jours

    Règle EMERGENCY (vérifiée en continu):
    - Toute stratégie avec DD > 20% → PAUSED immédiatement + alerte Discord

    Chaque transition est loggée avec: timestamp, stratégie, from_phase,
    to_phase, raison, métriques au moment de la transition.
    '''

    def check_promotions(self) -> list[Transition]:
        ...

    def check_degradations(self) -> list[Transition]:
        ...

    def emergency_check(self) -> list[Transition]:
        ...
"

Automatisation via Claude Code

Le lifecycle manager est un excellent exemple de code que Claude Code peut écrire en autonomie :

claude -p "Écris les tests unitaires pour StrategyLifecycleManager.
Scénarios à couvrir:
1. Promotion PAPER→PILOT après 14j avec bon sharpe
2. Promotion refusée si durée < 14j
3. Dégradation PRODUCTION→PAUSED si rolling_sharpe < 0
4. Emergency pause si DD > 20%
5. PAUSED→RETIRED après 90 jours
6. Double transition impossible (pas de PAPER→PRODUCTION directe)
7. Transition loggée correctement avec métriques
Utilise pytest + fixtures avec des données synthétiques."

Visualisation de l'allocation dans le temps

Le graphique ci-dessus montre l'évolution typique de l'allocation sur 12 mois. Notez comment :

Prochaine étape — Exécution & Market Microstructure

Maintenant que nous savons combien allouer à chaque stratégie et chaque position, il faut résoudre le comment : comment exécuter les ordres sans impact de marché ? Comment gérer le slippage, les frais, et les horaires multi-marchés ? C'est l'objet de la Partie 5.

Points clés de cette partie

  • Kelly fractionnel (Half-Kelly) pour le sizing optimal avec drawdown contrôlé
  • Shrinkage bayésien pour éviter le sur-sizing des stratégies jeunes
  • HRP pour la diversification sans inversion de matrice de covariance
  • Risk budgeting avec score de confiance multi-dimensionnel
  • 4 niveaux de contraintes : position, région, secteur, portefeuille
  • Circuit-breaker à -25% avec liquidation automatique + intervention humaine
  • Méta-allocateur : pipeline quotidien idempotent en 7 étapes
  • Lifecycle management : 7 phases de BACKTEST à RETIRED avec transitions automatiques
Partie suivante Exécution & Market Microstructure →
Algo Trading — De 100K au Million4/12