Summary
The trades-based Monte Carlo shuffles a backtest's closed trades and replays their absolute PNL against a running balance. Position size is normally derived from available capital, so a trade's absolute PnL only means something next to the balance it was sized against. In a compounding strategy a late trade carries a dollar amount the early balance could never have produced; replaying it first drives the balance below zero, and the drawdown calculation then reports figures past -100%.
Version: jesse 2.5.0.
Where
jesse/research/monte_carlo/monte_carlo_trades.py, in _reconstruct_equity_curve_from_trades:
current_balance = starting_balance
...
for _ in range(trades_to_add):
if trade_index < total_trades:
current_balance += shuffled_trades[trade_index]['PNL']
trade_index += 1
_ray_run_scenario_monte_carlo shuffles the trades and hands them straight to it, so a trade taken at a balance of 500,000 can land first, against 10,000.
Reproduction
Same strategy and settings, two windows:
| window |
final balance |
reported max_drawdown worst_5 |
| Binance Perp BTC-USDT, 2020-06 → 2026-07 |
10,000 → 139,080 |
-271% |
| Bitfinex Spot BTC-USD, 2017-08 → 2026-07 |
10,000 → 614,384 |
-2204% |
Nothing about the risk being taken changed between those two runs — only how long the account had to compound. The reported drawdown tracks the growth, not the risk, so it is not comparable across strategies, across settings of one strategy, or across window lengths.
A minimal illustration, starting_balance = 10000:
PNLS = [500, -300, 2000, -1500, 40000, -25000] # realised final: 25,700
# current behaviour, 300 shuffles
worst balance reached: -16,800 # -25,000 replayed against an early balance
The -25000 was taken when the account held roughly 52,000. Placed first, it takes the balance to -15,000, and _calculate_max_drawdown reports that faithfully as a drawdown beyond -100%.
Effect on the summary a user reads
max_drawdown is the metric the trades mode exists to estimate, and it is the one distorted. Because the distortion grows with account growth, the mode is least reliable exactly where it is most interesting — a strategy that compounded well over a long window.
The percentiles are also used by _calculate_confidence_intervals for p_value and is_significant_5pct, so the significance flags inherit the problem.
Suggested fix
Express each trade's PnL as a fraction of the balance it was sized against, in the original order, then compound the reordered fractions. The final value stays invariant to ordering, as it is today, while the path — and therefore the drawdown — still depends on it, which is what the simulation is measuring.
Two edge cases worth handling explicitly: a reordering can legitimately wipe the account out, which is a real outcome of that ordering and reads naturally as a 100% drawdown once the balance is floored at zero; and a run whose own sequence already reached zero leaves every later fraction undefined.
I have a patch with tests and will open a PR referencing this issue.
Note for fixed-notional strategies
For a strategy that trades a constant position size, absolute PnL is the right quantity to reorder, and the current behaviour is correct. The fraction-based version is the more generally safe default because sizing from available capital is what the framework's own guidance recommends (AGENTS.md: "Never use fixed quantities — use available sizing utilities"), but it is a modelling choice worth being explicit about.
Possibly related
While looking at this I noticed sharpe_ratio in the same mode is computed by _calculate_volatility_metrics with ANNUALIZATION_FACTOR = 365 over an equity curve rebuilt from trades rather than days, so the spacing assumption does not hold. And calmar_ratio mixes two formulas across the same table row, which I reported separately in #606.
Summary
The trades-based Monte Carlo shuffles a backtest's closed trades and replays their absolute
PNLagainst a running balance. Position size is normally derived from available capital, so a trade's absolute PnL only means something next to the balance it was sized against. In a compounding strategy a late trade carries a dollar amount the early balance could never have produced; replaying it first drives the balance below zero, and the drawdown calculation then reports figures past -100%.Version: jesse 2.5.0.
Where
jesse/research/monte_carlo/monte_carlo_trades.py, in_reconstruct_equity_curve_from_trades:_ray_run_scenario_monte_carloshuffles the trades and hands them straight to it, so a trade taken at a balance of 500,000 can land first, against 10,000.Reproduction
Same strategy and settings, two windows:
max_drawdownworst_5Nothing about the risk being taken changed between those two runs — only how long the account had to compound. The reported drawdown tracks the growth, not the risk, so it is not comparable across strategies, across settings of one strategy, or across window lengths.
A minimal illustration,
starting_balance = 10000:The
-25000was taken when the account held roughly 52,000. Placed first, it takes the balance to -15,000, and_calculate_max_drawdownreports that faithfully as a drawdown beyond -100%.Effect on the summary a user reads
max_drawdownis the metric the trades mode exists to estimate, and it is the one distorted. Because the distortion grows with account growth, the mode is least reliable exactly where it is most interesting — a strategy that compounded well over a long window.The percentiles are also used by
_calculate_confidence_intervalsforp_valueandis_significant_5pct, so the significance flags inherit the problem.Suggested fix
Express each trade's PnL as a fraction of the balance it was sized against, in the original order, then compound the reordered fractions. The final value stays invariant to ordering, as it is today, while the path — and therefore the drawdown — still depends on it, which is what the simulation is measuring.
Two edge cases worth handling explicitly: a reordering can legitimately wipe the account out, which is a real outcome of that ordering and reads naturally as a 100% drawdown once the balance is floored at zero; and a run whose own sequence already reached zero leaves every later fraction undefined.
I have a patch with tests and will open a PR referencing this issue.
Note for fixed-notional strategies
For a strategy that trades a constant position size, absolute PnL is the right quantity to reorder, and the current behaviour is correct. The fraction-based version is the more generally safe default because sizing from available capital is what the framework's own guidance recommends (
AGENTS.md: "Never use fixed quantities — use available sizing utilities"), but it is a modelling choice worth being explicit about.Possibly related
While looking at this I noticed
sharpe_ratioin the same mode is computed by_calculate_volatility_metricswithANNUALIZATION_FACTOR = 365over an equity curve rebuilt from trades rather than days, so the spacing assumption does not hold. Andcalmar_ratiomixes two formulas across the same table row, which I reported separately in #606.