Run Monte Carlo research¶
Use persistra.monte_carlo when a research question needs a distribution of paths and bounded
path-level outcomes. Keep data selection and calibration outside the experiment so those choices
remain visible.
Choose and calibrate a model¶
For geometric Brownian motion, first construct a complete log-return sample and choose its annualization explicitly:
import pandas as pd
from persistra.monte_carlo import fit_geometric_brownian_motion
log_returns = pd.DataFrame(
{
"stock": [0.004, -0.002, 0.006, 0.001],
"bond": [0.001, 0.002, -0.001, 0.001],
},
index=pd.date_range("2025-01-02", periods=4, freq="B"),
)
model = fit_geometric_brownian_motion(
log_returns,
initial_prices=pd.Series({"stock": 100.0, "bond": 100.0}),
periods_per_year=252.0,
)
MultivariateNormalReturns accepts caller-supplied annual mean and covariance parameters and can
emit simple or log returns. Normal simple returns are unbounded and can fall below negative one;
choose the model and parameters accordingly before using level-based or portfolio calculations.
MovingBlockBootstrap jointly samples complete historical rows and preserves within-row
cross-asset dependence. Its time steps must all equal one because each generated row represents
one historical observation rather than a fraction of a year.
Define the experiment¶
The output index, time-step sequence, and model variable names are exact ordered axes:
from persistra.monte_carlo import (
MaximumDrawdown,
MonteCarloExperiment,
TerminalReturn,
ThresholdBreach,
)
future_index = pd.date_range("2025-02-03", periods=20, freq="B", name="date")
experiment = MonteCarloExperiment(
model=model,
output_index=future_index,
time_steps=(1.0 / 252.0,) * len(future_index),
path_count=1_000,
root_seed=20250203,
metrics=(
TerminalReturn("stock", initial_level=100.0),
MaximumDrawdown("stock"),
ThresholdBreach("stock", threshold=90.0),
),
retain_paths=False,
convergence_checkpoints=(100, 250, 500),
)
The final path count is automatically included as a convergence checkpoint. The normal-approximate
confidence interval in result.summary estimates uncertainty in each metric mean; it is not a
prediction interval for an individual future outcome.
Run serially or in threads¶
from persistra.monte_carlo import MonteCarloExecution, run_experiment
result = run_experiment(
experiment,
MonteCarloExecution(backend="threaded", workers=4, batch_size=128),
)
print(result.summary)
print(result.convergence)
print(result.manifest)
Use serial execution for the simplest baseline. Threaded execution preserves exact path values and row order but may or may not improve elapsed time. Changing workers or batch size does not change experiment identity.
Retain and inspect paths¶
Set retain_paths=True when later work needs full paths. result.path_array(path_id) and
result.path_frame(path_id) return defensive copies. A retained array has axes
(path, time, variable).
evaluate_paths(result, evaluator) can apply a new bounded evaluator after generation. It rejects
results whose paths were not retained. For known evaluators, pass them directly to
run_experiment(..., evaluator=...) and leave retention disabled.
Evaluate portfolio outcomes¶
PortfolioBacktestEvaluator interprets each path explicitly as returns or prices and runs the
existing vectorized portfolio backtester. Target columns must exactly equal the model variable
axis, and both paths and targets must use compatible DatetimeIndex values.
from persistra.monte_carlo import PortfolioBacktestEvaluator
targets = pd.DataFrame(
[[0.6, 0.4]],
index=pd.DatetimeIndex([future_index[0]], name="date"),
columns=["stock", "bond"],
)
evaluator = PortfolioBacktestEvaluator(
targets,
transaction_cost_bps=pd.Series({"stock": 2.0, "bond": 1.0}),
path_kind="prices",
)
The evaluator returns terminal equity, terminal return, maximum drawdown, total turnover, and
total transaction cost. It retains none of the heavyweight per-path backtest objects. Portfolio
timing and missing/nontradeable policies remain explicit through optional BacktestTiming and
BacktestPolicies values. The default one-period execution lag also avoids holding a price path
on its initial, return-less observation.