<< Sommaire QC | Précédent : QC-Py-Cloud-07-TemporalCNN <<

Value Factor Z-Score — Sélection multi-facteurs fondamentaux

Source : Projet etudiant ESGF 2026 (QC 31932810), anonymise et adapte. Type : doc cloud — résultats obtenus sur QuantConnect Cloud.


Objectifs d’apprentissage

  1. Comprendre le Z-Score factoriel : normaliser des fondamentaux hétérogènes en un score composite.
  2. Distinguer 8 facteurs fondamentaux et leur sens économique (value vs growth, qualité vs rendement).
  3. Analyser pourquoi une stratégie value peut sous-performer pendant une décennie growth-dominée (2015-2024).
  4. Interpréter les limites d’un backtest : Sharpe faible, PSR < 1%, drawdown élevé.

Prérequis : Concepts de value investing, ratios financiers (P/E, ROE, FCF yield).

Durée estimée : 30 min

1. Concept : Le Z-Score multi-facteurs fondamental

L’idée est simple : au lieu de trier les actions sur un seul critère (ex: P/E le plus bas), on combine plusieurs facteurs fondamentaux en un score unique.

Les 8 facteurs

Facteur chemin fondamental Sens Direction favorisée
Revenue Growth operation_ratios.revenue_growth Croissance du chiffre d’affaires Supérieur = mieux
Gross Margin operation_ratios.gross_margin Marge brute Supérieur = mieux
ROE operation_ratios.roe Retour sur capitaux propres Supérieur = mieux
Operating Margin operation_ratios.operation_margin Marge opérationnelle Supérieur = mieux
P/E Ratio valuation_ratios.pe_ratio Prix / bénéfices Inférieur = mieux (value)
Book Yield valuation_ratios.book_value_yield Valeur comptable / prix Supérieur = mieux
Earning Yield valuation_ratios.earning_yield Bénéfices / prix Supérieur = mieux
FCF Yield valuation_ratios.fcf_yield Flux de trésorerie libres / prix Supérieur = mieux

Pourquoi normaliser avant de combiner ?

Les 8 facteurs ne vivent pas sur la même échelle : une marge brute est un ratio entre 0 et 1, un P/E se compte en dizaines, une croissance peut être négative. Additionner ces valeurs brutes donnerait un score dominé par le facteur aux plus grands nombres (le P/E écraserait tout). Le Z-Score ramène chaque colonne à une moyenne 0 et un écart-type 1 : chaque facteur pèse exactement le même poids statistique dans le composite, quels que soient ses ordres de grandeur d’origine.

Deux pièges classiques de la cross-section :

  • Les extrêmes écrasent la moyenne : un actif au P/E aberrant (proche de zéro, ou négatif sur un trimestre de perte) produit un z monstrueux. Les stratégies sérieuses winsorisent (bornent les z à ±3) ou excluent les fondamentaux non exploitables avant de calculer.
  • La normalisation est relative à l’univers : un z-score de +1.5 signifie « au-dessus de la moyenne du top 50 liquide », pas « bon en absolu ». Changer l’univers (top 50 → top 500) change tous les z-scores.

Méthode

  1. Sélection : prendre les N actions les plus liquides (top 50 par dollar volume).
  2. Z-Score : pour chaque facteur, calculer (valeur - moyenne) / écart-type.
  3. Inversion : le P/E est inversé (-z) car un P/E bas = value.
  4. Composite : moyenne des Z-Scores = score global.
  5. Sélection finale : garder les top 10.
  6. Allocation : équipondérée, rééquilibrage mensuel.

Exercice 1 : Calcul du Z-Score composite

Le Z-Score normalise chaque facteur pour les rendre comparables. Pour un vecteur de valeurs \(x_1, \ldots, x_n\) :

\[z_i = \frac{x_i - \mu}{\sigma}\]

Implémentez la fonction zscore_composite qui prend une matrice de facteurs (lignes = actifs, colonnes = facteurs) et retourne le score composite pour chaque actif.

Indices méthodologiques : utilisez np.nanmean / np.nanstd sur axis=0 — dans un univers réel, certains actifs n’ont pas tous leurs fondamentaux publiés, et une seule valeur manquante suffirait sinon à contaminer toute la colonne avec NaN. Bornez ensuite les z à ±3 avant la moyenne (winsorisation) : un P/E aberrant sur un actif ne doit pas dicter le classement. Enfin, l’argument invert_cols reçoit les indices des colonnes à inverser — dans le notebook QC, il vaut l’indice du P/E.

# Exercice 1 : Calcul du Z-Score composite
# TODO etudiant : implémenter zscore_composite(factors_matrix, invert_cols=None)
# Indice : np.nanmean + np.nanstd sur axis=0, puis moyenne sur axis=1
# invert_cols : liste des indices de colonnes à inverser (ex: PE ratio)
def zscore_composite(factors_matrix, invert_cols=None):
    pass  # TODO etudiant : implémenter le Z-Score composite

Exemple résolu : le composite en action sur un mini-univers

La fonction zscore_composite étant à compléter, voici la même mécanique appliquée pas à pas sur un univers simulé de 8 actifs et 4 facteurs (les ordres de grandeur hétérogènes sont volontaires : une croissance s’exprime en dixièmes, un P/E en dizaines). Observez comment la normalisation rend les colonnes comparables, comment l’inversion du P/E réorienté le facteur value, et surtout qui gagne à l’arrivée.

# Demonstration executee : Z-Score composite sur un mini-univers simule seedee
# (complement de l'Exercice 1 -- meme mecanique, appliquee pas a pas)

import numpy as np

rng = np.random.default_rng(8)
noms = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel"]
facteurs = ["croissance_CA", "marge_brute", "ROE", "PE_ratio"]
croissance = rng.normal(0.08, 0.06, 8)          # ordres de grandeur heterogenes
marge = rng.uniform(0.10, 0.55, 8)              # volontaires : dixiemes vs dizaines
roe = rng.normal(0.14, 0.06, 8)
pe = rng.uniform(9.0, 42.0, 8)

M = np.column_stack([croissance, marge, roe, pe])
z = (M - np.nanmean(M, axis=0)) / np.nanstd(M, axis=0)   # normalisation par colonne
z[:, 3] = -z[:, 3]                                       # inversion du P/E (bas = value)

composite = z.mean(axis=1)
ordre = np.argsort(-composite)
print("actif     | " + " | ".join(f"{f:>13s}" for f in facteurs) + " | composite")
for i in ordre:
    print(f"{noms[i]:9s} | " + " | ".join(f"{M[i, j]:13.3f}" for j in range(4)) + f" | {composite[i]:+9.3f}")
print()
print(f"Top 2 retenu par le Z-Score composite : {[noms[i] for i in ordre[:2]]}")
print(f"Actif au P/E le plus bas ({noms[np.argmin(pe)]}, PE {pe.min():.1f}) : composite {composite[np.argmin(pe)]:+.3f}")
print(f"Actif a la meilleure croissance ({noms[np.argmax(croissance)]}, {croissance.max():+.1%}) : composite {composite[np.argmax(croissance)]:+.3f}")
actif     | croissance_CA |   marge_brute |           ROE |      PE_ratio | composite
Hotel     |         0.134 |         0.290 |         0.162 |        16.361 |    +0.876
Bravo     |        -0.000 |         0.316 |         0.226 |        14.991 |    +0.719
Foxtrot   |         0.069 |         0.187 |         0.155 |        23.925 |    -0.001
Alpha     |        -0.024 |         0.148 |         0.137 |         9.554 |    -0.045
Delta     |         0.059 |         0.216 |         0.122 |        21.979 |    -0.059
Golf      |         0.023 |         0.466 |         0.042 |        29.042 |    -0.233
Charlie   |        -0.002 |         0.209 |         0.090 |        22.024 |    -0.502
Echo      |        -0.059 |         0.183 |         0.162 |        29.352 |    -0.754

Top 2 retenu par le Z-Score composite : ['Hotel', 'Bravo']
Actif au P/E le plus bas (Alpha, PE 9.6) : composite -0.045
Actif a la meilleure croissance (Hotel, +13.4%) : composite +0.876

Lecture de la sortie. Hotel domine le classement (composite +0.876) : croissance de +13.4 %, P/E contenu à 16.4 — il est bien noté sur les quatre facteurs à la fois, et le composite récompense cette régularité. La leçon la plus instructive est ailleurs : Alpha, l’actif au P/E le plus bas (9.6), ne finit qu’à −0.045. Vu au travers du seul critère value, Alpha serait le champion ; le composite le rétrograde parce que ses autres fondamentaux (croissance −2.4 %, ROE 13.7 %) sont dans la moyenne basse de l’univers. C’est exactement la fonction d’un score multi-facteurs : éviter les value traps — ces titres bon marché sur un seul ratio mais médiocres sur le reste. À l’inverse Bravo (croissance nulle, P/E 15.0, ROE 22.6 %) monte à +0.719 : la « qualité value » sans croissance reste un profil recherché. Enfin, comparez les colonnes brutes du tableau : sans normalisation, le P/E (9 à 42) écraserait numériquement la marge brute (0.10 à 0.55) dans toute somme — le Z-Score est ce qui rend les quatre colonnes comparables.

2. Le piège du value investing (2015-2024)

La période 2015-2024 a été dominée par les growth stocks (FAANG/Mag7, tech, IA). Les facteurs value (P/E bas, book yield élevé) ont systématiquement sous-performé.

Pourquoi ?

  • Taux bas : l’argent gratuit favorise les entreprises en croissance rapide. Le mécanisme est celui de la duration : une action value est un flux de dividendes proches et tangibles (duration courte), une action growth capitalise des flux lointains et espérés (duration longue). Quand les taux baissent, la valeur actualisée des flux lointains monte plus vite que celle des flux proches — le growth sur-performe mécaniquement.
  • Concentration : les Mag7 (Apple, Microsoft, Amazon, Google, Meta, Nvidia, Tesla) ont accaparé les rendements.
  • Intangibles : les fondamentaux comptables traditionnels sous-évaluent les actifs incorporels (R&D, marques, réseaux). Le book yield d’une plateforme logicielle ne dit rien de son avantage concurrentiel.
  • Momentum croissance : les actions growth ont bénéficié d’un cercle vertueux (appréciation → inflow → appréciation).

Résultat : la stratégie value Z-Score sur 2015-2024

Métrique Valeur Benchmark SPY (~) Interprétation
Sharpe 0.227 ~0.80-1.00 Sous-performance nette
CAGR 6.41% ~15% La moitié du marché
MaxDD -36.5% ~-25% (COVID) Drawdown plus sévère
PSR 0.8% > 50% Non significatif statistiquement

Leçon : une stratégie value à fondamentaux solides (8 facteurs diversifiés) peut tout de même sous-performer pendant une décennie entière. Le value n’est pas un signal universel — il est régime-dépendant. La remontée des taux de 2022 (fin de l’argent gratuit) a d’ailleurs commencé à replier ce régime : le value a repris du terrain relatif précisément quand la duration longue du growth est redevenue un risque.

Exercice 2 : Identifier les facteurs défavorisés dans un contexte growth

Certains de nos 8 facteurs sont particulièrement pénalisés en période growth. Identifiez lesquels et pourquoi.

Piste de structuration : classez d’abord les 8 facteurs en trois familles — valorisation (P/E, book yield, earning yield, FCF yield), qualité (marges, ROE), croissance (revenue growth) — et demandez-vous, pour chaque famille, si le régime 2015-2024 la payait ou la pénalisait. Attention à une subtilité : les quatre facteurs de valorisation ne sont pas quatre signaux indépendants — P/E bas et earning yield élevé sont deux écritures de la même information de prix. Un composite qui les additionne sur-pondère donc implicitement le « value » ; c’est un choix de design (ici assumé), pas un accident.

# Exercice 2 : Analyse des facteurs en contexte growth
# TODO etudiant : pour chaque facteur, indiquer si il est favorisé ou pénalisé
# en période growth (2015-2024) avec une courte justification
factors_analysis = {
    "revenue_growth": None,  # TODO etudiant : "favorisé" ou "pénalisé" + raison
    "gross_margin": None,
    "roe": None,
    "op_margin": None,
    "pe_ratio": None,
    "book_yield": None,
    "earning_yield": None,
    "fcf_yield": None,
}
# Indice : les facteurs de valorisation (P/E, book yield) pénalisent les growth stocks
print("Exercice a completer")
Exercice a completer

Correction guidée : les facteurs sous pression en régime growth

En cascade dans le contexte 2015-2024 :

  • Pénalisés — pe_ratio, book_yield, earning_yield, fcf_yield : les quatre facteurs de valorisation adorent les flux actuels et tangibles ; or la décennie a payé les flux futurs espérés. Sélectionner sur ces critères a systématiquement écarté les Mag7 du portefeuille — le coût d’opportunité EST la sous-performance.
  • Neutres à favorisés — gross_margin, op_margin, roe : les mesures de qualité ne discriminent pas growth vs value ; plusieurs géants tech ont d’excellentes marges, ce qui les aurait fait remonter dans un composite orienté qualité.
  • Favorisé — revenue_growth : le seul facteur dont le signe jouait à plein pour le régime de la décennie.

Le point à retenir : sur nos 8 facteurs, la moitié jouait contre le régime et une seule jouait pour. Un composite également pondéré hérite donc de ce biais de régime — ce que la section suivante mesure.

2b. Le piège look-ahead des données fondamentales

Avant de lire le code, une mise en garde qui concerne toute stratégie à fondamentaux : les ratios ne sont pas disponibles le jour où la période comptable se clôt. Une entreprise publie son trimestre avec 4 à 10 semaines de délai ; les operation_ratios et valuation_ratios de QuantConnect matérialisent ce délai (données laguées et polarisées : le ratio du T4 2021 n’entre dans l’univers que lorsqu’il est effectivement publié, début 2022).

Le piège du look-ahead se glisse quand on reconstruit soi-même les z-scores hors de l’engine : calculer le composite du 31 décembre avec les ratios « de l’exercice 2021 », c’est utiliser en décembre une information que le marché n’a connue qu’en février. Le backtest devient magique — et faux. Trois réflexes :

  1. Dater l’information, pas la période : chaque facteur doit être horodaté par sa date de publication, jamais par sa période de référence.
  2. Vérifier la source : l’API fundamentals de LEAN gère ce lag pour vous quand on la consomme depuis l’algorithm (self.fundamentals au rebalancement) ; un export CSV reconstruit à la main, non.
  3. Le test de survie : si votre backtest à fondamentaux affiche un Sharpe > 1.5 là où la littérature académique mesure 0.3-0.5, suspectez d’abord le look-ahead avant de vous féliciter.

C’est aussi ce qui explique en partie la modestie du Sharpe final (0.227) : la version honnête d’un composite value — sans information d’avance, avec les délais de publication réels — est structurellement moins brillante qu’un prototype qui triche sans le savoir.

3. Le code QC Cloud

La stratégie FundamentalZScoreAlgorithm (projet QC 31932810) :

class FundamentalZScoreAlgorithm(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2015, 1, 1)
        self.set_end_date(2024, 12, 31)
        self.set_cash(100_000)

        # 8 facteurs fondamentaux
        self._factor_names = [
            "revenue_growth", "gross_margin", "roe", "op_margin",
            "pe_ratio", "book_value_yield", "earning_yield", "fcf_yield",
        ]
        # Sélection : top 50 par liquidité, puis top 10 par Z-Score composite
        self._liquid_universe_size = 50
        self._final_universe_size = 10

Le cœur de la stratégie est dans _select_assets : 1. Tri par dollar_volume → top 50 liquides. 2. Extraction des 8 fondamentaux → matrice \(50 \times 8\). 3. Z-Score par colonne (inversion du P/E). 4. Composite = moyenne des Z-Scores. 5. Top 10 → équipondérée, rééquilibrage mensuel.

Deux détails d’implémentation qui comptent dans les résultats :

  • Le filtrage des données manquantes : un actif dont un facteur est indisponible au rebalancement est exclu du mois plutôt que comblé — un fillna(0) silencieux fabriquerait un z-artefact (un zéro n’est jamais neutre une fois normalisé : c’est une note de « moyenne » mensongère).
  • L’univers liquide recalculé chaque mois : le top 50 par dollar volume n’est pas figé ; les entrées/sorties créent du turnover, et donc des frais, que le net de la performance intègre.

4. Résultats QC Cloud

Projet QC Cloud : 31932810 (“Clone of Equity Aroon Trend” — nom trompeur, la classe est FundamentalZScoreAlgorithm). Période : 2015-01-01 → 2024-12-31 (10 ans).

Métrique Valeur
Sharpe 0.227
CAGR 6.41%
MaxDD -36.5%
PSR 0.8%
Tradeable days 2516

Interprétation

Sharpe 0.227 : en dessous du seuil de rentabilité ajustée au risque. SPY a un Sharpe ~0.8-1.0 sur la même période. La stratégie ne compense pas le risque pris.

CAGR 6.41% : moins de la moitié du rendement du marché (~15%). En termes réels (après inflation), le rendement est marginal.

MaxDD -36.5% : pire que le benchmark. La diversification sur 10 actions value n’a pas protégé pendant les crises (COVID 2020, taux montants 2022).

PSR 0.8% : le Probabilistic Sharpe Ratio est proche de zéro. Cela signifie que le Sharpe observé (0.227) n’est statistiquement pas distinguishable de zéro. Le signal est du bruit, pas de l’alpha.

Le PSR en une phrase

Le PSR (Bailey & López de Prado) estime la probabilité que le Sharpe vrai soit positif, compte tenu du Sharpe observé, de la longueur de l’échantillon (2516 jours) et de la forme de la distribution des rendements (skew et kurtosis — les deux pénalisent). Ici 0.8 % : même en faveur du doute statistique, il y a moins d’une chance sur cent que cette stratégie ait un véritable edge positif sur la période. C’est le genre de chiffre qui doit arrêter un déploiement, pas le faire attendre : à l’inverse d’un Sharpe élevé mais mesuré sur 60 jours (échantillon trop court), ici l’échantillon est long — c’est le signal lui-même qui est absent.

Conclusion : la stratégie fonctionne en théorie (value factor académiquement valide), mais échoue en pratique sur cette décennie spécifique. C’est un cas d’école du risque de régime dependency.

Exercice 3 : Proposer une amélioration régime-aware

Une stratégie value ne fonctionne pas dans tous les régimes de marché. Proposez un mécanisme simple pour désactiver le signal value quand le régime est défavorable.

Structure attendue : (1) un indicateur de régime mesurable (le squelette suggère le rendement relatif SPY vs VTV sur 252 jours) ; (2) une règle de décision avec seuil — pensez à l’hystérésis pour éviter les allers-retours au voisinage du seuil ; (3) l’action sur le portefeuille : désactivation brutale, ou gradation de l’exposition ? La correction guidée après la cellule détaille chacun de ces trois choix et leurs limites.

# Exercice 3 : Amélioration régime-aware
# TODO etudiant : implémenter detect_growth_regime(spy_prices, lookback=252) -> bool
# Indice : comparer le rendement de SPY vs un proxy value (ex: VTV ETF)
# Si SPY >> VTV sur lookback jours, le régime est growth -> désactiver le signal value
def detect_growth_regime(spy_prices, lookback=252):
    return False  # TODO etudiant : implémenter la détection de régime

print("Exercice a completer")
Exercice a completer

Correction guidée : un filtre de régime minimal

Le mécanisme attendu tient en trois ingrédients :

  1. Un spread relatif : rendement de SPY sur lookback jours moins rendement d’un proxy value (VTV). Le niveau absolu du marché ne dit rien du régime — c’est l’écart growth−value qui le mesure.
  2. Un seuil avec hystérésis : désactiver le signal value quand le spread dépasse un seuil haut (régime growth confirmé), le réactiver quand il repasse sous un seuil bas plus faible. Sans hystérésis, un régime frontalier ferait entrer/sortir la stratégie chaque semaine — coûts de transaction et whipsaw.
  3. Une action de gradation : plutôt qu’un on/off binaire, réduire la pondération du composite value et la basculer sur le benchmark — le filtre devient une exposition modulée, pas un interrupteur.

Deux limites à garder en tête (elles sont celles de tout filtre de régime) : le lookback 252 jours retarde le basculement d’environ un trimestre (le régime de mars 2020 serait détecté en été) ; et le filtre optimisé a posteriori sur 2015-2024 surfit par construction — il faut le valider sur une période hors échantillon, ou l’accepter comme garde-fou conservateur plutôt que comme source d’alpha.

5. Leçon principale : Le value n’est pas un signal universel

Points clés

  1. Le value factor est régime-dépendant : il fonctionne sur de longues périodes (Fama-French, 1992-2020) mais peut sous-performer pendant une décennie entière.

  2. Le Z-Score composite ne corrige pas le problème : normaliser des fondamentaux ne change pas le fait que le marché préfère la croissance à la valeur comptable. Le composite protège des value traps (cf. l’exemple résolu : le P/E le plus bas ne gagne pas), pas des régimes hostiles à toute la famille value.

  3. Le PSR est un filtre essentiel : un Sharpe de 0.227 avec PSR < 1% = signal non significatif. Toujours vérifier la significativité statistique avant de déployer.

  4. L’équipondérée sur 10 actions concentre le risque : en période de stress, les corrélations montent et la diversification s’évapore.

  5. Les fondamentaux traditionnels (P/E, book value) sous-évaluent les entreprises à actifs incorporels dominants (tech, plateforme, IA).

  6. Les données fondamentales sont laguées : la date de publication prime sur la période de référence (section 2b) — un backtest value qui brille trop a probablement un look-ahead.

Positionnement dans la série

Ce notebook est le pendant fondamental des Cloud-01 à Cloud-07 orientés prix/sentiment : il montre la même discipline de mesure (Sharpe/PSR/MaxDD face au benchmark) appliquée à une famille de signaux différente — et le verdict négatif honnête fait partie de l’enseignement. Les filtres de régime esquissés à l’Exercice 3 sont approfondis dans QC-Py-28-Market-Regime-Detection.

Pour aller plus loin

  • Fama-French 5-factor model : ajouter size, profitability, investment aux facteurs value et marché.
  • Regime detection : utiliser le momentum relatif (growth vs value ETFs) comme filtre.
  • Dynamic weighting : adapter l’allocation aux régimes détectés (value en régime value, growth en régime growth).

<< Sommaire QC | Précédent : QC-Py-Cloud-07-TemporalCNN <<

Retour au sommet