Kelly Criterion, Hierarchical Risk Parity, risk budgeting, contraintes multi-régions, allocation dynamique entre N stratégies, et gestion du drawdown maximum à 25%.
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.
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éthode | Paramètres à estimer | Stabilité des poids | Turnover | Sharpe OOS |
|---|---|---|---|---|
| Markowitz MVO | N² + N | Très instable | 200%+/an | 0.3-0.5 |
| Equal Weight (1/N) | 0 | Parfaitement stable | 20%/an | 0.4-0.6 |
| Risk Parity | N² | Stable | 40%/an | 0.5-0.7 |
| HRP | N² | Très stable | 35%/an | 0.6-0.9 |
| Kelly + HRP | N² + N | Stable | 50%/an | 0.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.
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.
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 Kelly | Croissance vs Full | Volatilité | Max DD typique | Recommandation |
|---|---|---|---|---|
| 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.
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 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 :
Imaginez deux stratégies :
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.
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.
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é.
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.
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.
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'''
"
| Méthode | CAGR | Sharpe | Max DD | Turnover | Calmar |
|---|---|---|---|---|---|
| Equal Weight | 12.4% | 0.65 | -34.2% | 18% | 0.36 |
| Risk Parity | 10.8% | 0.72 | -22.1% | 42% | 0.49 |
| Min Variance | 8.6% | 0.81 | -18.5% | 65% | 0.46 |
| HRP | 13.1% | 0.82 | -19.8% | 36% | 0.66 |
| Kelly + HRP | 18.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%.
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.
| Dimension | Score 0 | Score 0.5 | Score 1.0 | Poids |
|---|---|---|---|---|
| Nombre de trades | < 30 | 30-200 | > 200 | 30% |
| Stabilité OOS | Sharpe OOS < 0.3 × IS | 0.3-0.7 × IS | > 0.7 × IS | 35% |
| Ancienneté live | < 1 mois | 1-6 mois | > 6 mois | 20% |
| Cohérence économique | Aucune thèse claire | Thèse partielle | Thèse solide | 15% |
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.
'''
"
| Stratégie | Sharpe | Confiance | RB | Vol cible | Poids Risk-On | Poids Risk-Off |
|---|---|---|---|---|---|---|
| Cross-Section Momentum | 1.35 | 0.85 | 22% | 18% | 20% | 12% |
| Time-Series Momentum | 1.10 | 0.80 | 17% | 15% | 16% | 10% |
| Post-Earnings Drift | 1.45 | 0.72 | 20% | 22% | 15% | 8% |
| Industry Rotation | 0.95 | 0.88 | 16% | 12% | 14% | 10% |
| Mean Reversion RSI | 0.85 | 0.65 | 10% | 20% | 8% | 15% |
| Pairs Trading | 0.75 | 0.60 | 9% | 10% | 7% | 12% |
| Dual Momentum ETF | 0.90 | 0.92 | 16% | 8% | 12% | 18% |
| Sector Rotation | 0.80 | 0.78 | 12% | 14% | 8% | 15% |
| Cash buffer | — | 5-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%.
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 :
| Contrainte | Valeur | Raison |
|---|---|---|
| Max par position individuelle | 10% du NAV | Limiter le risque idiosyncratique |
| Min par position | 0.5% du NAV | Éviter les positions noise (frais > alpha) |
| Max par position small-cap (<$1B) | 3% du NAV | Risque de liquidité |
| Nombre max de positions | 40 | Attention/monitoring limités |
| Nombre min de positions | 12 | Diversification minimale |
| Région | Min | Max | Typique |
|---|---|---|---|
| US (NYSE, NASDAQ) | 40% | 70% | 55% |
| Europe (Euronext, LSE, Xetra) | 15% | 40% | 25% |
| APAC (TSE, HKEX, ASX) | 5% | 25% | 15% |
| ETF global | 0% | 30% | 5% |
| Secteur | Max | Raison |
|---|---|---|
| Technology | 30% | Corrélation intra-sectorielle élevée |
| Healthcare | 25% | Risque réglementaire concentré |
| Financials | 20% | Risque systémique |
| Energy | 15% | Cyclicité extrême |
| Tout autre secteur | 20% | Diversification |
| Contrainte | Valeur | Action si violation |
|---|---|---|
| Cash minimum | 5% du NAV | Réduire les positions les plus petites |
| Beta portefeuille max | 1.2 | Réduire les positions high-beta |
| Turnover max quotidien | 15% 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 |
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 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 :
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.
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
"
| Phase | Allocation | Durée min | Critères de passage | Critères de régression |
|---|---|---|---|---|
| 🔬 BACKTEST | 0% | — | Sharpe OOS > 0.7, Deflated SR > 0.5 | — |
| 📋 PAPER | 0% | 2 semaines | Tracking error < 5% vs backtest | Sharpe < 0.3 |
| 🟡 PILOT | 2% | 1 mois | Sharpe live > 0.5, DD < 10% | DD > 15% ou Sharpe < 0 |
| 🟠 RAMP_UP | 5% | 1 mois | Sharpe live > 0.6, cohérent avec paper | DD > 12% ou Sharpe < 0.2 |
| 🟢 PRODUCTION | Kelly × HRP | — | En continu (health check quotidien) | Rolling Sharpe 60d < 0 |
| ⏸️ PAUSED | 0% | 2 semaines | Retour Sharpe > 0.3 sur 2 semaines | 3 mois en pause → RETIRED |
| 🔴 RETIRED | 0% | — | Revue complète + refactoring | — |
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]:
...
"
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."
Le graphique ci-dessus montre l'évolution typique de l'allocation sur 12 mois. Notez comment :
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.