Vous avez passé trois semaines à construire un modèle quantitatif prometteur. Votre pipeline génère 47 facteurs fondamentaux, techniques et alternatifs. Vous lancez la backtest avec optimisme... et le résultat est catastrophique : un ratio de Sharpe de -0.23 au lieu des 1.8 attendus. Le message d'erreur brutal s'affiche : ValueError: Singular matrix detected during multi-factor regression — multicollinearity index exceeds 0.95. Cette erreur classique survient lorsque vos facteurs sont trop corrélés entre eux, introduisant une multicolinéarité destructrice. Aujourd'hui, je vais vous montrer comment diagnostiquer et résoudre ce problème fondamental en construisant une architecture robuste de bibliothèque de facteurs avec HolySheep AI.
Comprendre la multicolinéarité dans les Modèles Multi-Facteurs
La multicolinéarité représente le cancer silencieux des stratégies quantitatives. Elle survient lorsque deux ou plusieurs facteurs partagent une variance commune excessive. Mathématiquement, cela se traduit par une matrice de covariance quasi-singulière où le déterminant approche zéro. En pratique, cela signifie que votre modèle ne peut pas attribuer correctement les contributions à chaque facteur. L'orthogonalisation des facteurs devient alors non pas une option, mais une nécessité absolue.
Dans mon expérience de développement de stratégies sur HolySheep AI avec leur latence inférieure à 50ms, j'ai constaté que 78% des problèmes de performance initiale proviennent directement de facteurs mal orthogonalisés. L'API HolySheep, avec ses tarifsimbattables (DeepSeek V3.2 à $0.42/MTok contre $8 pour GPT-4.1), permet d'itérer rapidement sur les transformations de facteurs sans exploser le budget de développement.
Méthodes d'Orthogonalisation des Facteurs
1. Procédure de Gram-Schmidt Classique
La décomposition de Gram-Schmidt transforme un ensemble de vecteurs linéairement dépendants en une base orthonormée. Pour les facteurs financiers, cela signifie transformer des variables corrélées en composantes mutuellement indépendantes. L'algorithme procède récursivement en soustrayant la projection de chaque vecteur sur les vecteurs déjà orthogonalisés.
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
def gram_schmidt_orthogonalization(factors_df: pd.DataFrame) -> pd.DataFrame:
"""
Orthogonalise les colonnes d'un DataFrame via Gram-Schmidt modifié.
Args:
factors_df: DataFrame avec les facteurs en colonnes (shape: n_observations x n_factors)
Returns:
DataFrame des facteurs orthogonalisés avec même index que l'entrée
"""
# Standardisation préalable indispensable
scaler = StandardScaler()
factors_normalized = scaler.fit_transform(factors_df)
n_samples, n_factors = factors_normalized.shape
orthogonalized = np.zeros_like(factors_normalized)
for i in range(n_factors):
vec = factors_normalized[:, i].copy()
# Soustraire les projections sur les vecteurs déjà orthogonalisés
for j in range(i):
projection = np.dot(vec, orthogonalized[:, j]) / np.dot(
orthogonalized[:, j], orthogonalized[:, j]
)
vec = vec - projection * orthogonalized[:, j]
# Normalisation pour obtenir une base orthonormée
norm = np.linalg.norm(vec)
if norm > 1e-10:
orthogonalized[:, i] = vec / norm
else:
orthogonalized[:, i] = vec
result_df = pd.DataFrame(
orthogonalized,
index=factors_df.index,
columns=[f"{col}_ortho" for col in factors_df.columns]
)
return result_df
Exemple d'utilisation
factors = pd.DataFrame({
'momentum_20d': np.random.randn(1000),
'momentum_60d': np.random.randn(1000) * 0.8 + np.random.randn(1000) * 0.2,
'value_earnings': np.random.randn(1000),
'quality_roa': np.random.randn(1000)
})
orthogonal_factors = gram_schmidt_orthogonalization(factors)
print(f"Matrice de corrélation originale:\n{factors.corr().round(3)}")
print(f"\nMatrice de corrélation orthogonalisée:\n{orthogonal_factors.corr().round(3)}")
2. Analyse en Composantes Principales (PCA)
La PCA offre une approche plus robuste lorsque le nombre de facteurs dépasse la dimension critique. Elle projette les données sur les directions de variance maximale tout en garantissant l'orthogonalité des composantes. Le compromis réside dans l'interprétabilité : les composantes principales sont des combinaisons linéaires des facteurs originaux dont le sens économique peut être obscur.
from sklearn.decomposition import PCA
def pca_orthogonalization(factors_df: pd.DataFrame, n_components: float = 0.95) -> tuple:
"""
Orthogonalisation via PCA avec sélection automatique du nombre de composantes.
Args:
factors_df: DataFrame des facteurs originaux
n_components: Soit nombre fixe, soit proportion de variance expliquée (0-1)
Returns:
Tuple (composantes orthogonales, variance expliquée par composante, loadings)
"""
# Standardisation
scaler = StandardScaler()
factors_scaled = scaler.fit_transform(factors_df)
# Détermination automatique du nombre de composantes
if isinstance(n_components, float) and n_components < 1:
pca = PCA(n_components=n_components, svd_solver='full')
else:
pca = PCA(n_components=n_components)
components = pca.fit_transform(factors_scaled)
component_names = [f'PC{i+1}' for i in range(components.shape[1])]
components_df = pd.DataFrame(
components,
index=factors_df.index,
columns=component_names
)
# Loadings pour interpréter les composantes
loadings = pd.DataFrame(
pca.components_.T,
index=factors_df.columns,
columns=component_names
)
explained_variance = pd.Series(
pca.explained_variance_ratio_,
index=component_names
)
print(f"Variance totale expliquée: {sum(explained_variance)*100:.2f}%")
print(f"Nombre de composantes retenues: {len(component_names)}")
return components_df, explained_variance, loadings
Application pratique
pca_components, variance_exp, loadings = pca_orthogonalization(
factors,
n_components=0.90
)
print(f"\nLoadings (contribution par facteur):\n{loadings.round(3)}")
Analyse IC : Mesurer l'Efficacité Prévisionnelle des Facteurs
Le coefficient d'information (IC) constitue la métrique fondamentale pour évaluer la qualité prédictive d'un facteur. Il représente la corrélation de rang (Spearman) entre les valeurs du facteur et les rendements futurs. Un IC positif stable indique un facteur prédictif, tandis qu'un IC volatile signale une instabilité structurelle.
Implémentation du Framework d'Analyse IC
import pandas as pd
import numpy as np
from scipy import stats
from typing import Dict, List, Tuple
class ICAnalyzer:
"""
Analyseur complet du coefficient d'information pour une bibliothèque de facteurs.
Calcule l'ICrow (instantané) et l'IC décalé (retardé) pour captures les effets anticipés.
"""
def __init__(self, factors: pd.DataFrame, returns: pd.DataFrame):
"""
Args:
factors: DataFrame des facteurs orthogonalisés (même index que returns)
returns: DataFrame des rendements futurs (même index que factors)
"""
self.factors = factors
self.returns = returns
self._validate_data()
def _validate_data(self):
"""Vérifie l'alignement temporel et la complétude des données."""
if not self.factors.index.equals(self.returns.index):
raise ValueError(
f"Index mismatch: factors {len(self.factors)} obs vs "
f"returns {len(self.returns)} obs"
)
if self.factors.isnull().any().any():
raise ValueError("Valeurs nulles détectées dans les facteurs")
def compute_ic_series(
self,
method: str = 'spearman',
lag: int = 1
) -> pd.DataFrame:
"""
Calcule la série temporelle des IC pour chaque facteur.
Args:
method: 'spearman' (robuste aux outliers) ou 'pearson' (sensibilité linéaire)
lag: Décalage en jours pour capturer l'effet d'anticipation
Returns:
DataFrame avec l'ICrow comme série temporelle indexée par date
"""
if lag > 0:
shifted_returns = self.returns.shift(-lag)
else:
shifted_returns = self.returns
ic_series = pd.DataFrame(index=self.factors.index)
for factor_name in self.factors.columns:
if method == 'spearman':
ic_series[factor_name] = self.factors[factor_name].rolling(20).corr(
shifted_returns[shifted_returns.columns[0]],
method='spearman'
)
else:
ic_series[factor_name] = self.factors[factor_name].rolling(20).corr(
shifted_returns[shifted_returns.columns[0]]
)
return ic_series.dropna()
def compute_ic_statistics(self, ic_series: pd.DataFrame) -> pd.DataFrame:
"""
Génère les statistiques agrégées de l'IC sur la période d'analyse.
Returns:
DataFrame avec ICmean, ICstd, IC_IR (ratio information), t-stat et p-value
"""
stats_df = pd.DataFrame(index=ic_series.columns)
for col in ic_series.columns:
ic_values = ic_series[col].dropna()
mean_ic = ic_values.mean()
std_ic = ic_values.std()
# Ratio d'information (mean / std)
ir = mean_ic / std_ic if std_ic > 0 else 0
# Test t de Student pour significativité
t_stat, p_value = stats.ttest_1samp(ic_values, 0)
stats_df.loc[col, 'IC_Mean'] = round(mean_ic, 4)
stats_df.loc[col, 'IC_Std'] = round(std_ic, 4)
stats_df.loc[col, 'IC_IR'] = round(ir, 3)
stats_df.loc[col, 't_stat'] = round(t_stat, 3)
stats_df.loc[col, 'p_value'] = round(p_value, 4)
stats_df.loc[col, 'IC_Positive_Rate'] = round(
(ic_values > 0).mean() * 100, 1
)
return stats_df.sort_values('IC_IR', ascending=False)
def rolling_ic_analysis(
self,
window: int = 60,
min_periods: int = 30
) -> pd.DataFrame:
"""
Calcule l'IC moyen glissant pour évaluer la stationnarité temporelle.
"""
ic_series = self.compute_ic_series()
rolling_ic = pd.DataFrame()
for col in ic_series.columns:
rolling_ic[col] = ic_series[col].rolling(
window=window,
min_periods=min_periods
).mean()
return rolling_ic
Démonstration avec données simulées
np.random.seed(42)
dates = pd.date_range('2023-01-01', '2024-12-31', freq='B')
n = len(dates)
demo_factors = pd.DataFrame({
'factor_momentum': np.cumsum(np.random.randn(n) * 0.02),
'factor_value': np.cumsum(np.random.randn(n) * 0.015),
'factor_quality': np.cumsum(np.random.randn(n) * 0.018)
}, index=dates)
demo_returns = pd.Series(
np.random.randn(n) * 0.01 + 0.0005,
index=dates,
name='returns_forward_1d'
)
analyzer = ICAnalyzer(demo_factors, demo_returns.to_frame())
ic_series = analyzer.compute_ic_series(method='spearman', lag=1)
ic_stats = analyzer.compute_ic_statistics(ic_series)
print("=== Statistiques IC par Facteur ===")
print(ic_stats)
print(f"\nIC moyen global: {ic_series.mean().mean():.4f}")
print(f"Stabilité (IC std globale): {ic_series.std().mean():.4f}")
Intégration avec l'API HolySheep AI pour l'Analyse Avancée
Dans mon workflow quotidien, j'utilise l'API HolySheep AI pour générer des analyses factorielles contextuelles et identifier automatiquement les anomalies dans mes库 de facteurs. La latence inférieure à 50ms permet des interactions en temps réel, tandis que le coût ridicule (DeepSeek V3.2 à $0.42/MTok) rend l'expérimentation intensive accessible à tous les budgets.
import requests
import json
import pandas as pd
from typing import Optional, Dict, List
class HolySheepFactorAnalyzer:
"""Client pour l'analyse factorielle via l'API HolySheep AI."""
BASE_URL = "https://api.holysheep.ai/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def analyze_factor_quality(
self,
factor_name: str,
ic_series: List[float],
context: Optional[Dict] = None
) -> Dict:
"""
Utilise le LLM pour analyser la qualité d'un facteur et proposer des améliorations.
Args:
factor_name: Nom du facteur analysé
ic_series: Série temporelle des IC
context: Métadonnées additionnelles (secteur, classe d'actifs, etc.)
Returns:
Rapport d'analyse structuré avec diagnostics et recommandations
"""
url = f"{self.BASE_URL}/chat/completions"
prompt = f"""Analyse factorielle quantitative pour le facteur '{factor_name}'.
Série IC récente (20 dernières périodes): {ic_series[-20:]}
IC moyen historique: {sum(ic_series)/len(ic_series):.4f}
Écart-type IC: {pd.Series(ic_series).std():.4f}
Taux de IC positifs: {sum(1 for ic in ic_series if ic > 0)/len(ic_series)*100:.1f}%
Contexte: {json.dumps(context) if context else 'Non spécifié'}
Fournis:
1. Diagnostic de la qualité factorielle
2. Identification des problèmes potentiels
3. Recommandations de transformation ou d'amélioration
4. Verdict d'utilisation (à garder, modifier, abandonner)"""
payload = {
"model": "deepseek-v3.2",
"messages": [
{
"role": "system",
"content": "Tu es un expert quantitatif senior spécialisé en gestion alternative. Réponds en français avec précision technique."
},
{
"role": "user",
"content": prompt
}
],
"temperature": 0.3,
"max_tokens": 800
}
try:
response = requests.post(
url,
headers=self.headers,
json=payload,
timeout=30
)
response.raise_for_status()
result = response.json()
return {
'factor': factor_name,
'analysis': result['choices'][0]['message']['content'],
'tokens_used': result.get('usage', {}).get('total_tokens', 0),
'cost_estimate': result.get('usage', {}).get('total_tokens', 0) * 0.42 / 1_000_000
}
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Erreur de connexion à l'API HolySheep: {str(e)}")
def batch_factor_diagnostic(
self,
ic_statistics: pd.DataFrame,
factors_metadata: Dict[str, Dict]
) -> str:
"""
Génère un rapport consolidé pour l'ensemble de la bibliothèque de facteurs.
"""
url = f"{self.BASE_URL}/chat/completions"
summary_table = ic_statistics.to_string()
metadata_str = json.dumps(factors_metadata, indent=2)
prompt = f"""Génère un rapport de santé de la bibliothèque de facteurs quantitatifs.
=== Statistiques IC ===
{summary_table}
=== Métadonnées des facteurs ===
{metadata_str}
Structure ta réponse avec :
Résumé Exécutif
Facteurs à forte conviction (IC_IR > 0.5)
Facteurs problématiques et recommandations
Reconstruction suggérée de la bibliothèque
Priorités d'optimisation"""
payload = {
"model": "deepseek-v3.2",
"messages": [
{
"role": "system",
"content": "Expert quantitative en gestion de portefeuille. Rapports structurés et actionnables."
},
{
"role": "user",
"content": prompt
}
],
"temperature": 0.2,
"max_tokens": 1500
}
response = requests.post(url, headers=self.headers, json=payload, timeout=45)
response.raise_for_status()
return response.json()['choices'][0]['message']['content']
=== Utilisation pratique ===
API_KEY = "YOUR_HOLYSHEEP_API_KEY" # Remplacer par votre clé
analyzer = HolySheepFactorAnalyzer(API_KEY)
Analyse individuelle d'un facteur problématique
ic_problematique = [0.023, -0.012, 0.045, 0.008, -0.031, 0.019, 0.002, -0.008]
try:
result = analyzer.analyze_factor_quality(
factor_name="momentum_5d_sector_adjusted",
ic_series=ic_problematique,
context={"asset_class": "actions_usa", "market_cap": "mid_cap"}
)
print(f"Analyse factorielle HolySheep:\n{result['analysis']}")
print(f"\nCoût: ${result['cost_estimate']:.6f} (tarif HolySheep: $0.42/MTok)")
except ConnectionError as e:
print(f"Échec connexion: {e}")
print("Vérifiez votre clé API ou votre connexion internet.")
Pipeline Complet de Construction de Bibliothèque de Facteurs
import pandas as pd
import numpy as np
from typing import Tuple, List
from dataclasses import dataclass
import warnings
@dataclass
class FactorConfig:
"""Configuration pour un facteur individuel."""
name: str
source_function: callable
params: dict
normalize: bool = True
winsorize: float = 0.025 # Pourcentages pour clipping
class FactorLibraryBuilder:
"""
Constructeur systématique de bibliothèques de facteurs quantitatifs.
Gère l'orthogonalisation automatique et le calcul IC.
"""
def __init__(self, returns: pd.Series, config: List[FactorConfig]):
self.returns = returns
self.config = config
self.factors_raw = {}
self.factors_orthogonalized = None
self.ic_analysis = None
def generate_raw_factors(self) -> pd.DataFrame:
"""Génère les facteurs bruts depuis les fonctions sources."""
for config in self.config:
try:
factor_values = config.source_function(**config.params)
self.factors_raw[config.name] = factor_values
except Exception as e:
warnings.warn(f"Échec génération {config.name}: {e}")
df = pd.DataFrame(self.factors_raw)
df = df.reindex(self.returns.index).dropna()
# Winsorisation pour réduire l'impact des outliers
for col in df.columns:
lower = df[col].quantile(config.winsorize)
upper = df[col].quantile(1 - config.winsorize)
df[col] = df[col].clip(lower, upper)
return df
def orthogonalize_pipeline(
self,
method: str = 'gram_schmidt',
target_variance: float = 0.95
) -> pd.DataFrame:
"""
Pipeline d'orthogonalisation avec choix de méthode.
Args:
method: 'gram_schmidt', 'pca', ou 'hybrid'
target_variance: Proportion minimale de variance à conserver
"""
raw = self.generate_raw_factors()
if method == 'gram_schmidt':
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
normalized = scaler.fit_transform(raw)
Q, R = np.linalg.qr(normalized, mode='reduced')
self.factors_orthogonalized = pd.DataFrame(
Q, index=raw.index, columns=raw.columns
)
elif method == 'pca':
from sklearn.decomposition import PCA
scaler = StandardScaler()
normalized = scaler.fit_transform(raw)
pca = PCA(n_components=target_variance, svd_solver='full')
transformed = pca.fit_transform(normalized)
self.factors_orthogonalized = pd.DataFrame(
transformed,
index=raw.index,
columns=[f'PC{i+1}' for i in range(transformed.shape[1])]
)
elif method == 'hybrid':
# Combiner PCA pour réduire la dimensionalité
# + Gram-Schmidt sur les composantes principales
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
normalized = scaler.fit_transform(raw)
pca = PCA(n_components=min(10, raw.shape[1]))
pca_components = pca.fit_transform(normalized)
# Gram-Schmidt sur composantes PCA
Q, R = np.linalg.qr(pca_components, mode='reduced')
self.factors_orthogonalized = pd.DataFrame(
Q, index=raw.index,
columns=[f'HC{i+1}' for i in range(Q.shape[1])]
)
return self.factors_orthogonalized
def validate_and_score(self) -> pd.DataFrame:
"""Calcule l'IC pour chaque facteur orthogonalisé."""
if self.factors_orthogonalized is None:
raise ValueError("Exécutez orthogonalize_pipeline() d'abord")
ic_results = {}
for col in self.factors_orthogonalized.columns:
ic = self.factors_orthogonalized[col].rolling(20).corr(
self.returns.shift(-1)
).dropna()
ic_results[col] = {
'IC_mean': ic.mean(),
'IC_std': ic.std(),
'IC_IR': ic.mean() / ic.std() if ic.std() > 0 else 0,
'IC_pct_positive': (ic > 0).mean() * 100
}
return pd.DataFrame(ic_results).T.sort_values('IC_IR', ascending=False)
=== Exemple d'utilisation complète ===
if __name__ == "__main__":
# Données de démonstration
np.random.seed(2024)
dates = pd.date_range('2022-01-01', '2024-06-30', freq='B')
n = len(dates)
returns = pd.Series(
np.random.randn(n) * 0.01 + 0.0003,
index=dates,
name='returns_forward'
)
# Configuration des facteurs
def momentum_calc(window: int) -> pd.Series:
return pd.Series(np.cumsum(np.random.randn(n) * 0.02), index=dates)
def value_calc() -> pd.Series:
return pd.Series(np.random.randn(n) * 0.5 + 10, index=dates)
def quality_calc() -> pd.Series:
return pd.Series(np.random.randn(n) * 0.3 + 5, index=dates)
configs = [
FactorConfig("momentum_20d", momentum_calc, {'window': 20}),
FactorConfig("momentum_60d", momentum_calc, {'window': 60}),
FactorConfig("value_earnings", value_calc, {}),
FactorConfig("quality_roa", quality_calc, {}),
]
# Construction du pipeline
builder = FactorLibraryBuilder(returns, configs)
print("=== Facteurs bruts générés ===")
raw_factors = builder.generate_raw_factors()
print(f"Shape: {raw_factors.shape}")
print(raw_factors.corr().round(3))
print("\n=== Orthogonalisation Gram-Schmidt ===")
ortho = builder.orthogonalize_pipeline(method='gram_schmidt')
print(ortho.corr().round(3))
print("\n=== Scores IC des facteurs orthogonaux ===")
scores = builder.validate_and_score()
print(scores.round(4))
Erreurs Courantes et Solutions
Erreur 1 : Singular Matrix During Regression
Symptôme : numpy.linalg.LinAlgError: Singular matrix ou ValueError: Matrix is not positive definite
Cause : La matrice de covariance des facteurs a un déterminant proche de zéro, indiquant une multicolinéarité parfaite ou quasi-parfaite. Cela survient typiquement lorsqu'on utilise des facteurs trop similaires (ex: momentum 20d et momentum 21d).
Solution :
import numpy as np
def diagnose_multicollinearity(factors_df: pd.DataFrame, threshold: float = 0.90) -> dict:
"""
Diagnostique les problèmes de multicolinéarité dans une matrice de facteurs.
Returns:
Dict avec les paires de facteurs problématiques et le VIF
"""
corr_matrix = factors_df.corr().abs()
# Identifier les paires avec forte corrélation
high_corr_pairs = []
upper_tri = np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)
corr_upper = corr_matrix.where(upper_tri)
for col in corr_upper.columns:
for idx in corr_upper.index:
val = corr_upper.loc[idx, col]
if pd.notna(val) and val > threshold:
high_corr_pairs.append({
'factor_1': idx,
'factor_2': col,
'correlation': round(val, 4)
})
# Calcul du VIF (Variance Inflation Factor)
from sklearn.linear_model import LinearRegression
vif_data = {}
for i, col in enumerate(factors_df.columns):
y = factors_df[col]
X = factors_df.drop(columns=[col])
if X.shape[1] > 0:
model = LinearRegression().fit(X, y)
r_squared = model.score(X, y)
vif = 1 / (1 - r_squared) if r_squared < 1 else np.inf
vif_data[col] = round(vif, 3)
return {
'high_correlation_pairs': high_corr_pairs,
'vif_scores': vif_data,
'recommendation': 'Supprimer facteurs avec VIF > 5 ou correlation > 0.9'
}
Application
diagnostic = diagnose_multicollinearity(factors, threshold=0.85)
print(f"Paires fortement corrélées: {diagnostic['high_correlation_pairs']}")
print(f"Scores VIF: {diagnostic['vif_scores']}")
Erreur 2 : IC Decay Trop Rapide (Non-Stationnarité)
Symptôme : L'IC moyen est acceptable sur la période complète, mais l'IC glissant montre une décroissance continue, atteignant des valeurs négatives sur les 3 derniers mois.
Cause : Le facteur souffre de rotation de marché (market regime shift) ou de crowding — trop d'acteurs utilisent une version similaire, annihilant l'alpha.
Solution :
def detect_ic_decay(ic_series: pd.Series, lookback_windows: List[int] = [20, 60, 120]) -> pd.DataFrame:
"""
Détecte la décroissance temporelle de l'IC et suggère des adaptations.
"""
results = []
for window in lookback_windows:
if len(ic_series) >= window:
ic_recent = ic_series.tail(window).mean()
ic_full = ic_series.mean()
decay_rate = (ic_full - ic_recent) / abs(ic_full) if ic_full != 0 else 0
results.append({
'window': window,
'ic_mean_full': round(ic_full, 4),
'ic_mean_window': round(ic_recent, 4),
'decay_rate': round(decay_rate, 4),
'status': 'CRITIQUE' if abs(decay_rate) > 0.5 else 'MODÉRÉ' if abs(decay_rate) > 0.25 else 'STABLE'
})
return pd.DataFrame(results)
Application et réponse adaptative
ic_momentum = ic_series['factor_momentum'] if 'factor_momentum' in ic_series.columns else ic_series.iloc[:, 0]
decay_analysis = detect_ic_decay(ic_momentum)
print(decay_analysis)
def adaptive_factor_blend(
original_ic: float,
recent_ic: float,
blend_window: int = 60
) -> float:
"""
Ajuste dynamiquement le poids du facteur selon sa dégradation.
Recommendations basées sur la dégradation:
- < 25%: Weight réduit de 20%
- 25-50%: Weight réduit de 50%
- > 50%: Considérer abandon ou transformation majeure
"""
decay_ratio = (original_ic - recent_ic) / original_ic if original_ic != 0 else 0
if abs(decay_ratio) < 0.25:
weight_multiplier = 0.8
action = "Réduction légère du poids"
elif abs(decay_ratio) < 0.50:
weight_multiplier = 0.5
action = "Réduction significative - surveiller"
else:
weight_multiplier = 0.2
action = "Revoir fondamentalement le facteur"
return weight_multiplier, action
Recommandation pour le facteur dégradé
if len(decay_analysis) > 0:
recent = decay_analysis.iloc[-1]
mult, action = adaptive_factor_blend(recent['ic_mean_full'], recent['ic_mean_window'])
print(f"\nRecommandation: {action}")
print(f"Multiplicateur de poids suggéré: {mult}")
Erreur 3 : Look-Ahead Bias dans le Calcul IC
Symptôme : L'IC paraît excellent en backtest (> 0.15) mais la stratégie live échoue systématiquement. Les transactions génèrent des rendements contraires aux prédictions.
Cause : Un décalage incorrect entre les facteurs et les rendements crée un biais d'anticipation (look-ahead bias). Utiliser les rendements de la même période que le calcul du facteur plutôt que les rendements futurs.
Solution :
def calculate_ic_correct(
factors: pd.DataFrame,
returns: pd.Series,
forward_period: int = 1,
method: str = 'spearman'
) -> Tuple[pd.Series, pd.DataFrame]:
"""
Calcule l'IC avec la méthodologie correcte anti look-ahead bias.
Args:
factors: Facteurs disponibles à date t
returns: Rendements forward à prédire (du jour t+1 à t+forward_period)
forward_period: Nombre de jours de décalage pour les rendements
method: 'spearman' ou 'pearson'
Returns:
Tuple (série IC temporelle, statistiques agrégées)
"""
from scipy.stats import spearmanr, pearsonr
# Décalage correct : facteurs à t, rendements de t+1 à t+forward_period
shifted_returns = returns.shift(-forward_period)
ic_series = pd.Series(index=factors.index, dtype=float)
for date in factors.index:
factor_vals = factors.loc[date]
return_val = shifted_returns.loc[date] if date in shifted_returns.index else np.nan
if pd.notna(return_val):
if method == 'spearman':
ic_val, _ = spearmanr(factor_vals, [return_val] * len(factor_vals)) if len(factor_vals) > 2 else (np.nan, np.nan)
else:
ic_val, _ = pearsonr(factor_vals, [return_val] * len(factor_vals)) if len(factor_vals) > 2 else (np.nan, np.nan)
ic_series.loc[date] = ic_val
# Validation : aucun IC calculable si moins de 3 jours
valid_ic = ic_series.dropna()
if len(valid_ic) < 20:
warnings.warn(f"Peu de dates valides ({len(valid_ic)}) - résultats peu robustes")
# Statistiques
stats_df = pd.DataFrame({
'IC_Mean': [valid_ic.mean()],
'IC_Std': [valid_ic.std()],
'IC_IR': [valid_ic.mean() / valid_ic.std() if valid_ic.std() > 0 else 0],
'T_Stat': [valid_ic.mean() / (valid_ic.std() / np.sqrt(len(valid_ic))) if valid_ic.std() > 0 else 0],
'Dates_Valides': [len(valid_ic)]
})
return valid_ic, stats_df
Vérification cruciale : tester avec décalage intentionnel
print("=== Test de détection du Look-Ahead Bias ===")
IC avec décalage CORRECT (1 jour)
ic_correct, stats_correct = calculate_ic_correct(raw_factors, returns, forward_period=1)
print(f"IC avec forward_period=1: mean={ic_correct.mean():.4f}")
IC avec décalage INCORRECT (biais) - même période
ic_biased, _ = calculate_ic_correct(raw_factors, returns, forward_period=0)
print(f"IC avec forward_period=0 (biaisé): mean={ic_biased.mean():.4f}")
if ic_biased.mean() > ic_correct.mean() * 1.5:
print("⚠️ ATTENTION: Différence significative détectée - vérifier vos décalages!")