backtester/portfolio/calc/montecarlo.py montecarlo.py:
Bootstrap simulation for NAV bands, drawdown risk and probability queries.
This file contains the MonteCarloSimulation dataclass. It block-bootstraps historical returns into simulated paths, computes NAV and drawdown matrices, then exposes summary statistics and probability queries.
from backtester.portfolio.calc.montecarlo import MonteCarloSimulation When To Read This
- 01You need to explain how Monte Carlo results are generated from historical returns.
- 02You want probability of bust, probability of target return or horizon VaR.
- 03You are adding simulation outputs to PDF tear-sheets.
File Anatomy
- Dataclass configuration: returns, n_sims, block_size, horizon, initial_nav and seed.
- Simulation lifecycle: instantiate -> run() -> read nav_paths, drawdown_paths and summary.
- Block bootstrap: samples contiguous blocks to preserve some autocorrelation.
- Plot helper: builds fan chart with confidence bands and sample paths.
Data Contract
Inputs
- returns: pd.Series of period returns, usually daily strategy returns.
- n_sims, block_size, horizon, initial_nav and seed parameters.
- Optional plotting environment with matplotlib.
Outputs
- nav_paths: ndarray shape n_sims x horizon_plus_initial.
- drawdown_paths: ndarray with same shape as nav_paths.
- summary: dict with final NAV, return, CAGR, Sharpe and drawdown percentiles.
Invariants
- Call run() before reading probability queries or percentile paths.
- block_size=1 gives iid bootstrap; larger blocks preserve short-run dependence.
- The simulation is reproducible when seed is fixed.
Public API And Key Internals
MonteCarloSimulation
classMonteCarloSimulation(returns, n_sims=1000, block_size=21, horizon=None, initial_nav=1.0, seed=42) Dataclass holding simulation configuration and generated outputs.
run
methodrun() -> MonteCarloSimulation Runs block bootstrap, populates nav_paths, drawdown_paths and summary.
Returns
self for chaining.
bust_probability / goal_probability
methodbust_probability(threshold=-0.20) Answers probability questions from simulated drawdown or final return paths.
Returns
float.
var_at_horizon / percentile_path
methodpercentile_path(pct) Reads path or return percentiles from completed simulation results.
Returns
float or ndarray.
plot
methodplot(n_sample_paths=50, figsize=(11, 6)) Creates NAV fan chart with 5-95 and 25-75 percentile bands.
Returns
matplotlib Figure.
Implementation Notes
- The simulation stores full path matrices, not just summary values. That allows plots and follow-up probability queries.
- The path includes an initial NAV column before simulated periods, which matters for chart x-axis length.
- The method computes CAGR and Sharpe per simulated path, then summarizes distributions.
Code Walkthrough
Run simulation and ask probability questions
This is the public API a strategy packet or notebook would use.
from backtester.portfolio.calc.montecarlo import MonteCarloSimulation
mc = MonteCarloSimulation(
returns=strategy_returns,
n_sims=2_000,
block_size=21,
seed=42,
).run()
median_nav = mc.summary["median_final_nav"]
worst_band = mc.summary["dd_5pct"]
bust_30 = mc.bust_probability(threshold=-0.30)
goal_50 = mc.goal_probability(target=0.50)
fan_chart = mc.plot() Key implementation: block-bootstrap path construction
The simulator samples block starts, concatenates blocks and truncates to horizon.
rng = np.random.default_rng(self.seed)
r = self.returns.dropna().values
n = len(r)
T = self.horizon or n
bs = max(self.block_size, 1)
n_blocks = int(np.ceil(T / bs))
starts = rng.integers(0, n - bs + 1, size=(self.n_sims, n_blocks))
sim_returns = np.empty((self.n_sims, T))
for i in range(self.n_sims):
blocks = np.concatenate([r[s : s + bs] for s in starts[i]])
sim_returns[i] = blocks[:T] Key implementation: summary outputs
These values are what the report layer can serialize into metrics or plots.
final_nav = self.nav_paths[:, -1]
final_return = final_nav / self.initial_nav - 1
max_dd = self.drawdown_paths.min(axis=1)
self.summary = {
"median_final_nav": float(np.median(final_nav)),
"nav_5pct": float(np.percentile(final_nav, 5)),
"nav_95pct": float(np.percentile(final_nav, 95)),
"median_total_return": float(np.median(final_return)),
"median_cagr": float(np.median(cagr)),
"median_sharpe": float(np.median(sharpe)),
"worst_dd": float(np.min(max_dd)),
}