Prévoir des cours boursiers avec des simulations Monte Carlo
Introduction
En finance, on raisonne rarement sur un seul prix « prévu » : il s’agit plutôt de fourchettes, de risque de queue et de mesurer à quel point les modèles simples peuvent se tromper. Cet article déroule une simulation Monte Carlo de trajectoires en Python : on estime dérive et volatilité à partir des clôtures historiques, on simule de nombreux chemins de prix futurs (pas discret façon brownien géométrique), et on résume le tout par une distribution — l’objet adapté aux questions de risque (bandes, quantiles, recouvrement par rapport à une période tenue hors échantillon).
Le Monte Carlo par chaînes de Markov (MCMC), comme dans l’article de Landauskas et Valakevičius sur la modélisation des cours, est un autre outil : il tire des échantillons d’une loi qui n’a pas à être gaussienne simple — par exemple construite par estimation par noyau des prix observés — alors que le code ci-dessous suppose des chocs log-normaux à partir de dérive et volatilité estimées. Un flux pratique est MCMC (ou autre inférence) pour la loi des données, puis Monte Carlo forward pour les scénarios multi-périodes. Ce billet implémente explicitement le pas GBM forward ; voir les références et le lien WIP ci-dessous pour aller vers du MCMC « papier ».
Étape 1 : environnement Python
Installez les bibliothèques nécessaires (pandas, numpy, httpx, backtesting, pandas_ta, matplotlib, scipy, rich, etc.) :
import pandas as pd
import numpy as np
from datetime import datetime
import concurrent.futures
import warnings
from rich.progress import track
from backtesting import Backtest, Strategy
import pandas_ta as ta
import matplotlib.pyplot as plt
from scipy.stats import norm
import httpx
warnings.filterwarnings(“ignore”)
Étape 2 : fonctions utilitaires
Fonctions pour récupérer l’historique actions et crypto via API :
def make_api_request(api_endpoint, params):
with httpx.Client() as client:
# Make the GET request to the API
response = client.get(api_endpoint, params=params)
if response.status_code == 200:
return response.json()
print(“Error: Failed to retrieve data from API”)
return None
def get_historical_price_full_crypto(symbol):
api_endpoint = f"{BASE_URL_FMP}/historical-price-full/crypto/{symbol}"
params = {“apikey”: FMP_API_KEY}
return make_api_request(api_endpoint, params)
def get_historical_price_full_stock(symbol):
api_endpoint = f"{BASE_URL_FMP}/historical-price-full/{symbol}"
params = {“apikey”: FMP_API_KEY}
return make\_api\_request(api\_endpoint, params)
def get_SP500():
api_endpoint = “https://en.wikipedia.org/wiki/List_of_S%26P_500_companies”
data = pd.read_html(api_endpoint)
return list(data[0][‘Symbol’])
def get_all_crypto():
return [
“BTCUSD”, “ETHUSD”, “LTCUSD”, “BCHUSD”, “XRPUSD”, “EOSUSD”,
“XLMUSD”, “TRXUSD”, “ETCUSD”, “DASHUSD”, “ZECUSD”, “XTZUSD”,
“XMRUSD”, “ADAUSD”, “NEOUSD”, “XEMUSD”, “VETUSD”, “DOGEUSD”,
“OMGUSD”, “ZRXUSD”, “BATUSD”, “USDTUSD”, “LINKUSD”, “BTTUSD”,
“BNBUSD”, “ONTUSD”, “QTUMUSD”, “ALGOUSD”, “ZILUSD”, “ICXUSD”,
“KNCUSD”, “ZENUSD”, “THETAUSD”, “IOSTUSD”, “ATOMUSD”, “MKRUSD”,
“COMPUSD”, “YFIUSD”, “SUSHIUSD”, “SNXUSD”, “UMAUSD”, “BALUSD”,
“AAVEUSD”, “UNIUSD”, “RENBTCUSD”, “RENUSD”, “CRVUSD”, “SXPUSD”,
“KSMUSD”, “OXTUSD”, “DGBUSD”, “LRCUSD”, “WAVESUSD”, “NMRUSD”,
“STORJUSD”, “KAVAUSD”, “RLCUSD”, “BANDUSD”, “SCUSD”, “ENJUSD”,
]
def get_financial_statements_lists():
api_endpoint = f"{BASE_URL_FMP}/financial-statement-symbol-lists"
params = {“apikey”: FMP_API_KEY}
return make_api_request(api_endpoint, params)
Étape 3 : séparer entraînement et test
On récupère l’historique pour un symbole et on garde deux jeux : avant janvier 2023 (estimation du modèle et simulations) et à partir de janvier 2023 (hors échantillon pour comparer les plages simulées aux prix réalisés).
stock_symbol = “AAPL”
stock_prices = get_historical_price_full_stock(stock_symbol)
data = pd.DataFrame(stock_prices[‘historical’])
def prepare_price_frame(df):
df = df.rename(columns={
‘open’: ‘Open’,
‘high’: ‘High’,
’low’: ‘Low’,
‘close’: ‘Close’,
‘volume’: ‘Volume’,
})
required_columns = [‘date’, ‘Open’, ‘High’, ‘Low’, ‘Close’, ‘Volume’]
return df[required_columns].sort_values(by=[‘date’], ascending=True).reset_index(drop=True)
prices_before_january_2023 = prepare_price_frame(data[data[‘date’] < ‘2023-01-01’])
prices_after_january_2023 = prepare_price_frame(data[data[‘date’] >= ‘2023-01-01’])
plt.figure(figsize=(10, 6))
plt.title(‘Stock Prices’)
plt.xlabel(‘Date’)
plt.ylabel(‘Price’)
plt.plot(prices_before_january_2023[‘date’], prices_before_january_2023[‘Close’], label=‘Train (before Jan 2023)’)
plt.plot(prices_after_january_2023[‘date’], prices_after_january_2023[‘Close’], label=‘Hold-out (from Jan 2023)’)
plt.legend()
plt.show()


Étape 4 : simulation Monte Carlo (trajectoires forward et bandes de risque)
La fonction ci-dessous est une simulation Monte Carlo d’un modèle à paramètres constants : on estime moyenne et variance des rendements logarithmiques sur la fenêtre d’entraînement, on construit une dérive et une volatilité journalières, puis on tire de nombreux chocs gaussiens indépendants et on propage le prix vers l’avant. Ce n’est pas du MCMC ; il n’y a pas ici d’échantillonnage d’une loi a posteriori par chaîne de Markov. C’est le type de moteur de scénarios forward qu’on lance souvent après une étape d’inférence. À l’inverse, Landauskas et Valakevičius (Intellectual Economics, 2011) utilisent le MCMC pour échantillonner une loi façonnée par une estimation par noyau des prix (propositions linéaires par morceaux). Notre raccourci GBM est plus simple ; l’article est la référence pour l’étape d’échantillonnage proche des données.
Pour un travail en cours sur cette ligne (expériences batch, vues de risque plus riches, rapprochement d’un MCMC « papier »), voir cette expérimentation LinkedIn (WIP).
Les sorties utiles pour le risque sont des distributions : quantiles du prix terminal, bandes façon prédiction (par ex. chemins 5e–95e percentile), et contrôles de recouvrement sur l’hors échantillon (le prix réalisé tombait-il là où la masse simulée était ?).
def monte_carlo_simulation(data, days, iterations):
if isinstance(data, pd.Series):
data = data.to_numpy()
if not isinstance(data, np.ndarray):
raise TypeError(“Data must be a numpy array or pandas Series”)
log\_returns = np.log(data\[1:\] / data\[:-1\])
mean = np.mean(log\_returns)
variance = np.var(log\_returns)
drift = mean - (0.5 \* variance)
daily\_volatility = np.std(log\_returns)
future\_prices = np.zeros((days, iterations))
current\_price = data\[-1\]
for t in range(days):
shocks = drift + daily\_volatility \* norm.ppf(np.random.rand(iterations))
future\_prices\[t\] = current\_price \* np.exp(shocks)
current\_price = future\_prices\[t\]
return future\_prices


Visualisation
simulation_days = 364
mc_iterations = 1000
mc_prices = monte_carlo_simulation(prices_before_january_2023[‘Close’], simulation_days, mc_iterations)
last_train_close = prices_before_january_2023[‘Close’].iloc[-1]
last_close_price = np.full((1, mc_iterations), last_train_close)
mc_prices_combined = np.concatenate((last_close_price, mc_prices), axis=0)
last_date = prices_before_january_2023[‘date’].iloc[-1]
simulated_dates = pd.date_range(start=last_date, periods=simulation_days + 1)
# Percentiles across paths at each future step (risk band)
p05 = np.percentile(mc_prices_combined, 5, axis=1)
p50 = np.percentile(mc_prices_combined, 50, axis=1)
p95 = np.percentile(mc_prices_combined, 95, axis=1)
mean_path = mc_prices_combined.mean(axis=1)
# Terminal distribution at the last simulated step (VaR-style summaries)
terminal_prices = mc_prices_combined[simulation_days, :]
mean_terminal_price = float(np.mean(terminal_prices))
q5, q50, q95 = np.percentile(terminal_prices, [5, 50, 95])
terminal_return = terminal_prices / last_train_close - 1.0
ret_q5, ret_q50, ret_q95 = np.percentile(terminal_return, [5, 50, 95])
horizon_idx = min(simulation_days, len(prices_after_january_2023) - 1)
real_price = float(prices_after_january_2023[‘Close’].iloc[horizon_idx])
real_date = prices_after_january_2023[‘date’].iloc[horizon_idx]
in_90_band = q5 <= real_price <= q95
print(f"Simulated horizon: {simulation_days} trading days after {last_date}")
print(f"Mean terminal price: ${mean_terminal_price:.2f}")
print(f"Terminal price percentiles (5 / 50 / 95): ${q5:.2f} / ${q50:.2f} / ${q95:.2f}")
print(f"Terminal simple return vs last train close — 5th / 50th / 95th %ile: {ret_q5*100:.2f}% / {ret_q50*100:.2f}% / {ret_q95*100:.2f}%")
print(f"Hold-out price at aligned step ({real_date}): ${real_price:.2f}")
print(f"Realized price inside simulated 5–95% band: {in_90_band}")
plt.figure(figsize=(10, 6))
for i in range(mc_iterations):
plt.plot(simulated_dates, mc_prices_combined[:, i], linewidth=0.5, color=‘gray’, alpha=0.02)
plt.fill_between(simulated_dates, p05, p95, alpha=0.25, label=‘5th–95th percentile band’)
plt.plot(simulated_dates, p50, label=‘Median path’, linewidth=2, color=‘C0’)
plt.plot(simulated_dates, mean_path, label=‘Mean path’, linewidth=2, linestyle=’–’, color=‘C1’)
plt.plot(pd.to_datetime(prices_before_january_2023[‘date’]), prices_before_january_2023[‘Close’], label=‘Train (before Jan 2023)’, linewidth=2, color=‘black’)
plt.plot(pd.to_datetime(prices_after_january_2023[‘date’]), prices_after_january_2023[‘Close’], label=‘Hold-out (from Jan 2023)’, linewidth=2, color=‘green’)
plt.axvline(pd.to_datetime(real_date), color=‘red’, linestyle=’:’, linewidth=1, alpha=0.8, label=‘Hold-out step aligned to horizon’)
plt.scatter([pd.to_datetime(real_date)], [real_price], color=‘red’, s=40, zorder=5, label=‘Realized (aligned)’)
plt.title(‘Monte Carlo Simulation of Stock Prices (with percentile band)’)
plt.xlabel(‘Date’)
plt.ylabel(‘Price’)
plt.legend(loc=‘upper left’, fontsize=8)
plt.show()




Conclusion
Le Monte Carlo forward fournit une distribution de prix futurs sous une dynamique supposée — adapté aux bandes de quantiles, au comportement de queue et aux contrôles de recouverture sur données hors échantillon. C’est une étape distincte du MCMC, qui sert à échantillonner sous un modèle flexible des données (comme l’approche KDE de Landauskas et Valakevičius) avant ou en parallèle de la simulation forward. Pipeline typique : ajuster ou échantillonner la loi qui colle à l’historique, puis faire avancer les scénarios par Monte Carlo. Avec du backtest de stratégie, on sépare « à quel point le risque de modèle est large ? » de « une règle est-elle rentable ? »
Code d’exploration lié : AlgoETS/MarkokChainMonteCarlo (expériences MCMC / stratification).
Références
- Landauskas, M. & Valakevičius, E. (2011). Modelling of Stock Prices by Markov Chain Monte Carlo Method — Intellectual Economics, Vol. 5 No. 2 (article page); PDF download.
- Semantic Scholar index for the same paper.
- Neural Networks in Finance: Markov Chain Monte Carlo (MCMC) and Stochastic Volatility Modeling
- Monte Carlo Simulation Basics
Publié à l’origine sur Medium.