Est-il vraiment impossible d’avoir 5% de rendement sur 40 ans ? J’ai testé 5151 portefeuilles pour le savoir

Formations Raphaxelo — Algo Trading, Fondamental & Options

28 modules ProOrder, outils quantitatifs privés, code source inclus. Pense. Code. Investis.

Voir les formations →
Avertissement sur les risques

Les options et les CFD sont des instruments complexes et présentent un risque élevé de perte rapide en capital en raison de l'effet de levier. 72 % des comptes d'investisseurs particuliers perdent de l'argent lorsqu'ils investissent sur les CFD avec IG. Vous devez vous assurer que vous comprenez le fonctionnement des CFD et que vous pouvez vous permettre de prendre le risque élevé de perdre votre argent.

TL;DR : Sur 5151 allocations testées entre actions US, obligations US et or (données réelles 1985-2025), même la pire combinaison possible (100% obligations) atteint 5,86% de rendement annualisé composé sur 40 ans. La meilleure allocation en rendement pur (72% actions / 28% or) atteint 9,36%, et la meilleure en rapport rendement/risque (33% actions / 54% obligations / 13% or, ratio de Sharpe 0,92) atteint 8% avec seulement 8,7% de volatilité. Méthode : balayage exhaustif (pas Monte Carlo) de toutes les allocations par pas de 1%, calcul du vrai CAGR composé (pas une moyenne arithmétique), code Python complet fourni. Limites principales : données brutes sans frais ni fiscalité, source de l’or reconstituée par patchwork avant 2000, une seule trajectoire historique, aucune garantie sur l’avenir.

Un micro résumé pour analysé sous un angle différent

📩

La Newsletter Raphaxelo

Stratégies quantitatives, backtests et algo trading — directement dans ta boîte mail.

Chargement des dernières newsletters…

Soutenir le Labo

Ce contenu est gratuit et indépendant. Un café pour garder la machine en marche ?

Offrir un café →

Salut les loulous 👋

Semaine dernière j’ai eu pas mal de remarques sur les résultats des marchés : plusieurs d’entre vous m’ont dit qu’il était “impossible” d’avoir du 5% de rendement annualisé sur 40 ans. Vous me connaissez, j’ai voulu vérifier ça par moi-même plutôt que de laisser trainer une intuition.

Alors j’ai sorti Python, et j’ai fait tourner une vraie analyse historique. Voici la démarche, ce que j’ai trouvé, et surtout (parce que c’est important) les limites de ce que je vous montre. Et pour ceux que ça intéresse, tout le code est en bas de la newsletter, aucune boîte noire ici.


La démarche

J’ai pris trois grandes classes d’actifs sur environ 40 ans d’historique réel (1985 à 2025) :

  • Actions US (S&P 500)
  • Obligations US (fonds Trésor long terme, total return, coupons réinvestis)
  • Or

Plutôt que de tester une seule allocation (genre le fameux “60/40”), j’ai fait tester à l’ordinateur toutes les combinaisons possibles entre ces trois actifs, par pas de 1%, soit 5151 allocations différentes, de 100% actions à 100% or en passant par tous les mélanges intermédiaires. Aucun tirage au hasard, aucun angle mort : littéralement toutes les répartitions possibles.

Pour chaque allocation, j’ai calculé le vrai rendement composé (CAGR) sur toute la période. Pas une moyenne simplifiée qui aurait tendance à survaloriser les résultats, mais la croissance réelle qu’aurait connue 100€ investis pendant 40 ans, avec toute la volatilité du chemin parcouru.

Le choix fait sur l’or

Petite parenthèse honnête : c’est la partie la plus galère de l’exercice. Contrairement aux actions ou aux obligations, il n’existe pas de source gratuite et fiable donnant le prix de l’or sur 40 ans de façon simple. J’ai dû enchaîner plusieurs sources (Stooq, un ticker Yahoo alternatif, et en dernier recours un indice d’actions minières aurifères pour combler les données avant 2000) pour reconstituer une série complète.

Ce n’est pas parfait : la portion la plus ancienne de la série “or” ne reflète pas exactement le prix spot du métal, mais plutôt des actions de sociétés minières, qui sont plus volatiles. Je le précise parce que ça compte pour interpréter les résultats qui suivent.

Partenaires recommandés
🦉

Hiboo Expert

L’outil d’analyse fondamentale incontournable pour évaluer la santé financière d’une action.

🎁 14 jours d’essai gratuits — Code Raphaxelo
Tester Hiboo
IG.

Connecter ProRealTime à IG

Exécute tes algorithmes avec ProRealTime et investis sur les options et les CFD via IG.

🎁 Accès ProRealTime offert sous conditions
Ouvrir un compte
📊

Moning

L’outil ultime pour suivre ton portefeuille et projeter tes dividendes futurs en un coup d’œil.

🎁 Compte gratuit avec mon lien
Créer un compte

Petit rappel : c’est quoi la “frontière efficiente” ?

Je vous en avais parlé dans une newsletter précédente, petit retour dessus pour ceux qui ont zappé.

L’idée vient d’un économiste, Harry Markowitz, dans les années 1950. Son constat : pour un même niveau de rendement visé, il existe plusieurs façons de répartir son argent entre différents actifs, mais toutes ces répartitions n’ont pas le même niveau de risque. Certaines combinaisons sont “meilleures” que d’autres, au sens où on obtient le même rendement pour moins de risque, ou plus de rendement pour le même risque.

Plus de détails ici

Algo & Backtest : Les Coulisses du Trading AutomatiséDans la Tête de Harry Markowitz

Raphaxelo

·

23 juil.

Dans la Tête de Harry Markowitz

Oui je sais, aujourd’hui on ne va pas parler d’un investisseur au sens promoteur, mais j’ai eu l’idée lors de l’écriture de mon livre sur l’optimisation de portefeuille (je partage chapitre par chapitre tous les mois) via python

Lisez l’intégralité de l’article

Si on trace tous les portefeuilles possibles sur un graphique (risque en abscisse, rendement en ordonnée), on obtient un nuage de points. La frontière efficiente, c’est la limite supérieure de ce nuage : la courbe des portefeuilles qui offrent le meilleur rendement possible pour chaque niveau de risque donné. Tout ce qui est en dessous de cette courbe est “sous-optimal” : on pourrait faire mieux pour le même risque.

Un peu de maths pour les curieux (rien d’insurmontable, promis) :

  • Le rendement attendu d’un portefeuille, c’est juste la moyenne pondérée des rendements de chaque actif : Rendement = w1 × R1 + w2 × R2 + w3 × R3 (où les w sont les poids de chaque actif, et la somme des poids fait 100%)
  • Le risque (la volatilité), lui, ne se calcule pas aussi simplement, parce qu’il faut aussi prendre en compte la corrélation entre les actifs. Deux actifs qui bougent dans des directions différentes se compensent partiellement, ce qui réduit le risque global du portefeuille. C’est tout l’intérêt de la diversification : ce n’est pas juste “ne pas mettre tous ses œufs dans le même panier”, c’est mathématiquement démontrable que ça réduit le risque sans sacrifier le rendement, à condition que les actifs ne soient pas parfaitement corrélés entre eux.

C’est ce concept qui est derrière le nuage de points et la courbe rouge pointillée que vous avez pu voir dans mes graphiques précédents.

Les résultats

Graphique de Markowitz montrant la frontière efficiente, le ratio Sharpe Max et la Variance Min

Sur les 5151 allocations testées :

CAGR Volatilité Sharpe Minimum 5,86% 8,55% 0,25 Maximum 9,36% 26,26% 0,92 Moyenne 8,16% 13,36% 0,65 Médiane 8,23% 12,75% 0,66

Petite précision pour les débutants : le ratio de Sharpe mesure le rendement obtenu par unité de risque prise. Plus il est élevé, meilleur est le compromis rendement/risque. Ce n’est pas forcément le portefeuille qui rapporte le plus qui a le meilleur Sharpe : parfois, un rendement un peu plus modeste mais beaucoup plus stable gagne largement au change.

Quelques allocations qui ressortent :

  • La pire allocation possible (100% obligations) fait quand même 5,86% de rendement annualisé composé sur 40 ans.
  • La meilleure allocation en rendement pur (72% actions / 28% or) grimpe à 9,36%, mais avec une volatilité de 26%. Ça secoue fort.
  • La meilleure allocation en rapport rendement/risque (33% actions / 54% obligations / 13% or) fait “seulement” 8% de CAGR, mais avec une volatilité contenue à 8,7%. C’est le meilleur compromis du lot, celui qui se rapproche le plus de la frontière efficiente.

Donc pour répondre à la remarque de la semaine dernière : sur cet historique précis, même la pire combinaison possible entre ces trois actifs dépasse les 5%. Le 5% n’était pas hors d’atteinte, c’était plutôt le plancher bas de l’exercice.

Le faisceau : voir toutes les allocations d’un coup d’œil

Un tableau de chiffres c’est bien, mais visualiser les 5151 allocations en même temps, c’est encore plus parlant. Voici le résultat :

Ce graphique demande un peu d’habitude, alors décortiquons-le. Chaque bande de couleur représente une tranche de la distribution des 5151 allocations à chaque instant du temps : la zone grise très pâle, c’est l’étendue complète (du pire au meilleur portefeuille), les bandes bleues plus ou moins foncées montrent où se concentre la majorité des allocations (du 10e au 90e percentile, puis du 25e au 75e). Les lignes fines colorées en fond (du violet au jaune, selon leur ratio de Sharpe) sont un échantillon de 150 allocations individuelles, juste pour donner une texture visuelle au faisceau.

Ce qui saute aux yeux : l’écart entre le haut et le bas de l’enveloppe se creuse fortement avec le temps. Sur les premières années, toutes les allocations se ressemblent à peu près. Mais après 20 ou 30 ans, l’effet de composition fait exploser les écarts : une allocation légèrement plus risquée mais mieux rémunérée finit par creuser un écart de plusieurs centaines de points de base par rapport à une allocation plus prudente. On voit aussi que la ligne “Médiane” (en bleu) traîne clairement en dessous du “Sharpe Max” (orange) et de la “Moyenne” (violet), preuve que la moitié des 5151 allocations testées font moins bien qu’un portefeuille correctement diversifié. Autrement dit, le hasard d’une allocation “au pif” a plus de chances de vous coûter cher sur 40 ans qu’un choix réfléchi entre rendement et risque.

Un disclaimer important

Je tiens à être clair, parce que ce genre de résultat peut vite donner un excès de confiance :

  • Le passé ne garantit rien sur l’avenir. 40 ans d’histoire, c’est une seule trajectoire parmi une infinité de futurs possibles. Rien ne dit que les 40 prochaines années ressembleront aux précédentes.
  • Aucun frais, aucune fiscalité, aucune inflation n’est pris en compte. Ces chiffres sont bruts. Dans la vraie vie, frais de gestion, impôts et inflation grignotent une bonne partie de ces rendements.
  • La donnée “or” est un patchwork, comme expliqué plus haut, surtout avant 2000, où j’ai dû utiliser un proxy imparfait.
  • Aucun coût de rééquilibrage n’est simulé, alors qu’en pratique, maintenir une allocation cible dans le temps a un coût (frais, fiscalité sur les arbitrages).
  • Ce n’est pas un conseil en investissement. C’est un exercice de vérification personnelle, pas une recommandation.

Restons sceptiques

Je vous partage ça pour la démarche, pas pour vous convaincre de quoi que ce soit. Le fait que même la pire combinaison dépasse 5% sur cette période précise ne veut pas dire que ça se reproduira. Les 40 dernières années ont vécu la désinflation post Volcker, l’essor massif de la tech, plusieurs crises (2000, 2008, 2020) suivies de reprises fortes, un contexte particulier qui ne se répétera pas forcément à l’identique.

Bref : intéressant à regarder, à prendre avec du recul, et surtout, comme toujours, faites vos propres vérifications avant de tirer des conclusions définitives sur votre épargne. 😉

Petite note perso pour finir : je continue à avancer sur mon livre sur l’optimisation de portefeuille, et ça progresse plutôt bien. Ce genre d’exercice pratique me sert d’ailleurs directement de matière pour certains chapitres. Je vous tiendrai au courant dès que j’aurai plus de visibilité sur la sortie.

À la prochaine,


Le code, pour la transparence

Comme promis, voici l’intégralité du code Python utilisé pour cette analyse. Rien de caché, vous pouvez le relancer chez vous et vérifier par vous-mêmes.

# ============================================================
# Analyse comparative de portefeuille à la Markowitz
# Actions US / Obligations US / Or, historique ~40 ans
# Balayage EXHAUSTIF de toutes les allocations possibles
# ============================================================

import os
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize

print("Version yfinance :", yf.__version__)

# ------------------------------------------------------------
# 1. Paramètres
# ------------------------------------------------------------
start_date = "1985-01-01"
end_date   = "2026-01-01"

# ------------------------------------------------------------
# 2. Fonction de téléchargement robuste (Yahoo Finance)
# ------------------------------------------------------------
def safe_download_close(ticker, start, end, label):
    df = yf.download(ticker, start=start, end=end, progress=False, auto_adjust=True)
    if df is None or df.empty:
        raise RuntimeError(f"[{label}] Aucune donnée retournée pour le ticker '{ticker}'.")
    if isinstance(df.columns, pd.MultiIndex):
        if "Close" in df.columns.get_level_values(0):
            close = df["Close"]
            if isinstance(close, pd.DataFrame):
                close = close.iloc[:, 0]
        else:
            raise RuntimeError(f"[{label}] Colonne 'Close' introuvable dans {df.columns.tolist()}")
    else:
        if "Close" not in df.columns:
            raise RuntimeError(f"[{label}] Colonne 'Close' introuvable dans {df.columns.tolist()}")
        close = df["Close"]
    close = close.squeeze()
    if not isinstance(close, pd.Series):
        raise RuntimeError(f"[{label}] Résultat inattendu (type={type(close)}), pas une Series.")
    close = close.dropna()
    if close.empty:
        raise RuntimeError(f"[{label}] Série vide après nettoyage pour '{ticker}'.")
    close.name = label
    print(f"[{label}] OK, {ticker} : {close.index.min().date()} -> {close.index.max().date()} "
          f"({len(close)} points)")
    return close

# ------------------------------------------------------------
# 3. Téléchargement Actions et Obligations
# ------------------------------------------------------------
actions = safe_download_close("^GSPC", start_date, end_date, "Actions_US")
obligations = safe_download_close("VUSTX", start_date, end_date, "Obligations")

# ------------------------------------------------------------
# 4. Or : cascade de tentatives, avec repli sur CSV local,
#    puis extension avec ^XAU si la période avant 2000 manque
# ------------------------------------------------------------
gold = None

try:
    gold_raw = pd.read_csv("https://stooq.com/q/d/l/?s=xauusd&i=d")
    if gold_raw.empty or "Close" not in gold_raw.columns:
        raise ValueError("Réponse Stooq vide ou invalide")
    gold_raw["Date"] = pd.to_datetime(gold_raw["Date"])
    gold = gold_raw.set_index("Date")["Close"].dropna()
    gold.name = "Or"
    print(f"[Or] OK, Stooq XAUUSD : {gold.index.min().date()} -> {gold.index.max().date()} "
          f"({len(gold)} points)")
except Exception as e:
    print(f"[Or] Stooq échoué ({e})")

if gold is None:
    try:
        gold = safe_download_close("XAUUSD=X", start_date, end_date, "Or")
    except Exception as e:
        print(f"[Or] XAUUSD=X échoué ({e})")

if gold is None:
    local_path = "gold_prices.csv"
    if os.path.exists(local_path):
        try:
            gold_raw = pd.read_csv(local_path)
            gold_raw.columns = [c.strip().lower() for c in gold_raw.columns]
            date_col = next(c for c in gold_raw.columns if "date" in c)
            price_col = next(c for c in gold_raw.columns if c != date_col)
            gold_raw[date_col] = pd.to_datetime(gold_raw[date_col])
            gold_raw[price_col] = pd.to_numeric(
                gold_raw[price_col].astype(str).str.replace(r"[^\d.\-]", "", regex=True),
                errors="coerce"
            )
            gold = gold_raw.dropna(subset=[price_col]).set_index(date_col)[price_col]
            gold.name = "Or"
            print(f"[Or] OK, CSV local '{local_path}' : {gold.index.min().date()} -> "
                  f"{gold.index.max().date()} ({len(gold)} points)")
        except Exception as e:
            print(f"[Or] Lecture du CSV local échouée ({e})")
            gold = None
    else:
        print(f"[Or] '{local_path}' introuvable dans le dossier courant.")

if gold is None:
    print("[Or] Repli sur GC=F (Yahoo Finance, historique ~2000+ seulement)")
    gold = safe_download_close("GC=F", start_date, end_date, "Or")

if gold is None or gold.empty:
    raise RuntimeError("Impossible d'obtenir des données sur l'or. Voir messages ci-dessus.")

cutoff_2000 = pd.Timestamp("2000-01-01")
if gold.index.min() > cutoff_2000:
    print(f"[Or] Historique ne remonte qu'à {gold.index.min().date()}, tentative d'extension "
          f"avec ^XAU jusqu'à 2000")
    try:
        xau = safe_download_close("^XAU", start_date, gold.index.min().strftime("%Y-%m-%d"), "XAU_temp")
        junction_date = gold.index.min()
        scale = gold.iloc[0] / xau.iloc[-1]
        xau_rebased = (xau * scale).rename("Or")
        combined = pd.concat([xau_rebased[xau_rebased.index < junction_date], gold])
        combined = combined[~combined.index.duplicated(keep="last")].sort_index()
        gold = combined
        print(f"[Or] Étendu avec ^XAU (rebasé) : {gold.index.min().date()} -> {junction_date.date()} "
              f"(jonction), puis source principale au-delà")
        print("     Attention : avant la jonction, la série reflète les actions minières (^XAU), "
              "pas le prix spot de l'or.")
    except Exception as e:
        print(f"[Or] Extension via ^XAU impossible ({e}), historique reste limité à partir de "
              f"{gold.index.min().date()}")

# ------------------------------------------------------------
# 5. Fusion des trois séries
# ------------------------------------------------------------
data = pd.concat([actions, obligations, gold], axis=1, join="inner")
data = data.dropna()

if data.empty:
    raise RuntimeError(
        "Le DataFrame final est vide après fusion, les séries ne se chevauchent "
        "peut-être pas dans le temps. Vérifiez les plages de dates affichées ci-dessus."
    )

print(f"\nPériode couverte réellement : {data.index.min().date()} -> {data.index.max().date()}")
n_years_total = (data.index[-1] - data.index[0]).days / 365.25
print(f"Nombre d'années : {n_years_total:.1f}")
print(f"Nombre d'actifs : {data.shape[1]}, nombre d'observations : {data.shape[0]}")
print(data.head())

# ------------------------------------------------------------
# 6. Rendements quotidiens et CAGR individuel par actif (vrai CAGR composé)
# ------------------------------------------------------------
returns_daily = data.pct_change().dropna()
n_years = len(returns_daily) / 252

cagr = {}
for col in data.columns:
    total_return = data[col].iloc[-1] / data[col].iloc[0]
    cagr[col] = total_return ** (1 / n_years) - 1

vol_annual = returns_daily.std() * np.sqrt(252)

summary = pd.DataFrame({
    "CAGR (%)": {k: round(v * 100, 2) for k, v in cagr.items()},
    "Volatilité annualisée (%)": (vol_annual * 100).round(2)
})
print("\n=== Résumé par actif ===")
print(summary)

print("\n=== Corrélations ===")
print(returns_daily.corr().round(2))

# ------------------------------------------------------------
# 7. Matrice de covariance / rendements moyens (pour l'optimiseur Markowitz)
# ------------------------------------------------------------
cov_annual = returns_daily.cov() * 252
mean_returns_annual = returns_daily.mean() * 252
n_assets = data.shape[1]
asset_names = list(data.columns)

# ------------------------------------------------------------
# 8. Fonctions Markowitz
# ------------------------------------------------------------
def portfolio_perf(weights, mean_returns, cov_matrix):
    ret = np.dot(weights, mean_returns)
    vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
    return ret, vol

def neg_sharpe(weights, mean_returns, cov_matrix, rf=0.0):
    ret, vol = portfolio_perf(weights, mean_returns, cov_matrix)
    return -(ret - rf) / vol

def portfolio_vol(weights, mean_returns, cov_matrix):
    return portfolio_perf(weights, mean_returns, cov_matrix)[1]

bounds = tuple((0, 1) for _ in range(n_assets))
constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
init_guess = np.array([1 / n_assets] * n_assets)

res_minvol = minimize(portfolio_vol, init_guess,
                       args=(mean_returns_annual, cov_annual),
                       method="SLSQP", bounds=bounds, constraints=constraints)

res_maxsharpe = minimize(neg_sharpe, init_guess,
                          args=(mean_returns_annual, cov_annual),
                          method="SLSQP", bounds=bounds, constraints=constraints)

def print_portfolio(name, weights, mean_returns, cov_matrix):
    ret, vol = portfolio_perf(weights, mean_returns, cov_matrix)
    print(f"\n--- {name} ---")
    for tk, w in zip(asset_names, weights):
        print(f"  {tk}: {w*100:.1f}%")
    print(f"  Rendement attendu (arithm.) : {ret*100:.2f}%")
    print(f"  Volatilité : {vol*100:.2f}%")
    print(f"  Sharpe (rf=0) : {ret/vol:.2f}")

print_portfolio("Portefeuille Variance Minimale", res_minvol.x, mean_returns_annual, cov_annual)
print_portfolio("Portefeuille Sharpe Maximal", res_maxsharpe.x, mean_returns_annual, cov_annual)

# ------------------------------------------------------------
# 9. Balayage EXHAUSTIF de toutes les allocations possibles
# ------------------------------------------------------------
step = 1  # pas en % (1 = teste 0%, 1%, 2%, ... 100% pour chaque actif)

assert n_assets == 3, "Ce grid exhaustif est écrit pour 3 actifs. Adapter la boucle si plus/moins."

weights_list = []
for w1 in range(0, 101, step):
    for w2 in range(0, 101 - w1, step):
        w3 = 100 - w1 - w2
        weights_list.append([w1 / 100, w2 / 100, w3 / 100])

weights_matrix = np.array(weights_list)
n_portfolios = len(weights_matrix)
print(f"\nNombre total de combinaisons testées : {n_portfolios}")

port_daily_returns = returns_daily.values @ weights_matrix.T

cum_log_returns = np.log1p(port_daily_returns).sum(axis=0)
cagr_sim = np.exp(cum_log_returns / n_years) - 1

vol_sim = port_daily_returns.std(axis=0) * np.sqrt(252)
sharpe_sim = cagr_sim / vol_sim

results = np.vstack([vol_sim, cagr_sim, sharpe_sim])

target_returns = np.linspace(mean_returns_annual.min(), mean_returns_annual.max(), 50)
frontier_vol = []
for target in target_returns:
    cons = (
        {"type": "eq", "fun": lambda w: np.sum(w) - 1},
        {"type": "eq", "fun": lambda w, target=target: portfolio_perf(w, mean_returns_annual, cov_annual)[0] - target}
    )
    res = minimize(portfolio_vol, init_guess, args=(mean_returns_annual, cov_annual),
                    method="SLSQP", bounds=bounds, constraints=cons)
    frontier_vol.append(res.fun if res.success else np.nan)

# ------------------------------------------------------------
# 10. Visualisation, toutes les allocations possibles
# ------------------------------------------------------------
plt.figure(figsize=(10, 6))
sc = plt.scatter(results[0]*100, results[1]*100, c=results[2], cmap="viridis", alpha=0.5, s=8)
plt.colorbar(sc, label="Ratio de Sharpe (CAGR composé)")
plt.plot(np.array(frontier_vol)*100, target_returns*100, "r--", linewidth=2,
         label="Frontière efficiente théorique (rendement arithmétique)")

minvol_ret, minvol_vol = portfolio_perf(res_minvol.x, mean_returns_annual, cov_annual)
maxsharpe_ret, maxsharpe_vol = portfolio_perf(res_maxsharpe.x, mean_returns_annual, cov_annual)

plt.scatter(minvol_vol*100, minvol_ret*100, c="blue", marker="*", s=300, label="Variance Min (théorique)")
plt.scatter(maxsharpe_vol*100, maxsharpe_ret*100, c="red", marker="*", s=300, label="Sharpe Max (théorique)")

plt.xlabel("Volatilité annualisée (%)")
plt.ylabel("Rendement annualisé (%)")
plt.title(f"Toutes les allocations possibles (pas de {step}%), Markowitz ({data.index[0].year}-{data.index[-1].year})")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

# ============================================================
# 11. Tableau récapitulatif de TOUTES les allocations testées
# ============================================================
recap = pd.DataFrame({
    "CAGR (%)": [cagr_sim.min()*100, cagr_sim.max()*100, cagr_sim.mean()*100, np.median(cagr_sim)*100],
    "Volatilité (%)": [vol_sim.min()*100, vol_sim.max()*100, vol_sim.mean()*100, np.median(vol_sim)*100],
    "Sharpe": [sharpe_sim.min(), sharpe_sim.max(), sharpe_sim.mean(), np.median(sharpe_sim)],
}, index=["Min", "Max", "Moyenne", "Médiane"])

print(f"\n=== Tableau récapitulatif sur les {n_portfolios} allocations testées (CAGR composé) ===")
print(recap.round(2))

idx_min_cagr    = np.argmin(cagr_sim)
idx_max_cagr    = np.argmax(cagr_sim)
idx_max_sharpe  = np.argmax(sharpe_sim)
idx_median_cagr = np.argmin(np.abs(cagr_sim - np.median(cagr_sim)))
idx_mean_cagr   = np.argmin(np.abs(cagr_sim - cagr_sim.mean()))

selected_indices = {
    "Min (CAGR le plus bas)":  idx_min_cagr,
    "Max (CAGR le plus haut)": idx_max_cagr,
    "Sharpe Max":              idx_max_sharpe,
    "Médiane":                 idx_median_cagr,
    "Moyenne":                 idx_mean_cagr,
}

print("\n=== Compositions des allocations sélectionnées (CAGR composé réel) ===")
for name, idx in selected_indices.items():
    w = weights_matrix[idx]
    comp = ", ".join([f"{tk}: {wi*100:.0f}%" for tk, wi in zip(asset_names, w)])
    print(f"{name:25s} | CAGR={cagr_sim[idx]*100:5.2f}% | Vol={vol_sim[idx]*100:5.2f}% | "
          f"Sharpe={sharpe_sim[idx]:4.2f} | {comp}")

# ============================================================
# 12. Graphique de performance cumulée des 5 allocations types
# ============================================================
plt.figure(figsize=(12, 7))

colors = {
    "Min (CAGR le plus bas)": "firebrick",
    "Max (CAGR le plus haut)": "seagreen",
    "Sharpe Max": "darkorange",
    "Médiane": "steelblue",
    "Moyenne": "purple",
}

for name, idx in selected_indices.items():
    w = weights_matrix[idx]
    port_daily_ret = returns_daily.dot(w)
    cum_perf = (1 + port_daily_ret).cumprod() * 100
    plt.plot(cum_perf.index, cum_perf.values, label=name, color=colors[name], linewidth=1.8)

plt.yscale("log")
plt.xlabel("Date")
plt.ylabel("Valeur du portefeuille (base 100, échelle log)")
plt.title("Performance cumulée des allocations Min / Max / Sharpe / Médiane / Moyenne\n(CAGR composé réel, balayage exhaustif)")
plt.legend()
plt.grid(alpha=0.3, which="both")
plt.tight_layout()
plt.show()

# ============================================================
# 13. Graphique en FAISCEAU, dispersion de TOUTES les allocations
# ============================================================
cum_all = (1 + port_daily_returns).cumprod(axis=0) * 100

p10  = np.percentile(cum_all, 10, axis=1)
p25  = np.percentile(cum_all, 25, axis=1)
p50  = np.percentile(cum_all, 50, axis=1)
p75  = np.percentile(cum_all, 75, axis=1)
p90  = np.percentile(cum_all, 90, axis=1)
p_min = cum_all.min(axis=1)
p_max = cum_all.max(axis=1)

dates = returns_daily.index

plt.figure(figsize=(13, 8))

plt.fill_between(dates, p_min, p_max, color="gray", alpha=0.12, label="Étendue min-max (toutes allocations)")
plt.fill_between(dates, p10, p90, color="steelblue", alpha=0.15, label="10e-90e percentile")
plt.fill_between(dates, p25, p75, color="steelblue", alpha=0.30, label="25e-75e percentile")
plt.plot(dates, p50, color="steelblue", linewidth=1.5, linestyle="--", label="Médiane (toutes allocations)")

n_sample = 150
np.random.seed(0)
sample_idx = np.random.choice(n_portfolios, size=min(n_sample, n_portfolios), replace=False)

sharpe_norm = (sharpe_sim[sample_idx] - sharpe_sim.min()) / (sharpe_sim.max() - sharpe_sim.min())
cmap = plt.cm.viridis

for i, idx in zip(sample_idx, sharpe_norm):
    plt.plot(dates, cum_all[:, i], color=cmap(idx), alpha=0.15, linewidth=0.6)

sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=sharpe_sim.min(), vmax=sharpe_sim.max()))
sm.set_array([])
cbar = plt.colorbar(sm, ax=plt.gca(), pad=0.01)
cbar.set_label("Ratio de Sharpe (échantillon de courbes fines)")

for name, idx in selected_indices.items():
    plt.plot(dates, cum_all[:, idx], color=colors[name], linewidth=2.2, label=name, zorder=5)

plt.yscale("log")
plt.xlabel("Date")
plt.ylabel("Valeur du portefeuille (base 100, échelle log)")
plt.title(f"Faisceau de toutes les allocations possibles ({n_portfolios} combinaisons)\n"
          f"Enveloppe statistique + 5 allocations types en surbrillance")
plt.legend(loc="upper left", fontsize=9, framealpha=0.9)
plt.grid(alpha=0.25, which="both")
plt.tight_layout()
plt.show()
Aidez-moi à diffuser le message, partagez !
À propos de l'auteur

Raphaxelo

Je code des algorithmes, je backteste des stratégies et je partage ce que la data révèle vraiment sur les marchés. Zéro promesse de richesse — que de la méthode, du code et des chiffres vérifiables.

Algo Trading ProRealTime Analyse Fondamentale Python Options & Greeks
Voir les formations

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *