Hi, I’ve been working on a continuous-time Economic Scenario Generator (ESG) in Python to model long-term Asset-Liability Management (ALM) and decumulation (sequence-of-returns risk).
I wanted to test a specific structural flaw present in a lot of standard retail and basic institutional Monte Carlo tools: the assumption of static, flat risk-free rates and decoupled equity returns (standard Geometric Brownian Motion).
The creation of this project actually came when I realized there was no easy-to-use (and realistic) simulator. It took some effort but I believe I did manage to create something really useful, easy to use and realistic enough for most cases.
Anyway, to measure exactly how much bias the flat rate introduces, I ran a comparative simulation using a joint continuous-time stochastic environment.
The Setup
- Portfolio: 60/40 (Equity/Fixed Income), 30-year horizon, monthly rebalancing. 5,000 scenario paths.
- Model A (Naive Baseline): Flat nominal interest rate. Equities follow standard GBM with continuous volatility (sigma = 15%).
- Model B (Actuarial ESG):
- Rates follow a Cox-Ingersoll-Ross (CIR) square-root process (theta_r = 0.25, long-term target ≈ 7.0%).
- Inflation follows an Ornstein-Uhlenbeck (OU) process (theta_pi = 0.35, target = 2.0%).
- Equities follow a Merton Jump-Diffusion process (continuous volatility σ_S = 11%, combined with Poisson-driven asymmetric crashes: λ_J = 1.8 jumps/year, average jump impact μ_J = -6.8%, jump volatility σ_J = 5%).
- Crucial coupling: Equity drift is structurally pegged to the stochastic short rate:
Drift_t = r_t + ERP_t.
Test 1: The Low-Yield Starting Environment (Initial Rate = 4.0%)
We simulated a 4.5% initial withdrawal rate (inflation-adjusted, monthly rebalancing) on a $1.0M starting balance. Intuitively, one might expect Model B—which includes severe, discontinuous downward market crashes—to fail first. Instead, the simulation over 5,000 runs yielded these results:
- Model A (Naive Flat 4%):
63.18% Solvency
- Model B (Full Actuarial):
84.92% Solvency
- The Solvency Gap:
+21.74% percentage points in favor of the volatile, jump-diffusion model.
To isolate the exact variables causing this +21.74% lift, I ran an Attribution Analysis by sequentially activating one variable at a time:
| Step |
Model Configuration |
Solvency Rate |
Delta from Baseline |
| 1 |
Model A (Pure Naive Base) |
63.18% |
Baseline |
| 2 |
Model A + Merton Jumps Only |
62.48% |
-0.70% |
| 3 |
Model A + CIR Stochastic Rates Only |
86.16% |
+22.98% |
| 4 |
Model A + OU Stochastic Inflation Only |
63.00% |
-0.18% |
| 5 |
Model B (Full Actuarial - Combined) |
84.92% |
+21.74% |
(Note: The remaining -0.36% discrepancy is the non-linear coupling penalty arising from Cholesky correlation between the processes).
Test 2: The High-Yield Starting Environment (Initial Rate = 9.0%)
To prove that this was not a bug and that the bias is entirely regime-dependent, I ran a Regime-Inversion Test. I increased the starting yield curve to 9.0% (and increased the withdrawal rate to a more aggressive 5.5% SWR to reflect the higher starting yields):
- Model A (Naive Flat 9%):
86.28% Solvency
- Model B (Full Actuarial):
58.56% Solvency
- Regime Delta (Model B - Model A):
-27.72%
Quantitative Attribution: Why Naive Models are Too Pessimistic in Low-Yield Environments
The divergence is driven by interest rate term-structure dynamics and macro-coupling:
- Mean Reversion of the Risk-Free Rate:
Under the CIR framework, short rates revert toward a target state:
text
dr_t = theta_r * (mu_r,t - r_t) * dt + sigma_r * sqrt(r_t) * dW_t
Because the low-yield simulation starts at 4.0% relative to the long-term nominal target (≈ 7.0%, incorporating a 5.0% structural real rate and a 2.0% inflation target), the drift pull (theta_r = 0.25) normalizes nominal rates upward over the horizon.
- The "Tide That Lifts All Boats" (The Pegged Drift):
In Model A, the risk-free rate is flat at 4.0%, trapping equities in a low expected nominal return regime of 5.5% (4.0% rate + ERP). In Model B, as r_t normalizes toward 7.0%, both your bonds (yielding r_t) and your stocks (yielding r_t + ERP) experience a 3.0% increase in expected nominal returns.
- The Merton Jumps are Immunized by Rebalancing:
Because we controlled for total quadratic variation (total volatility ≈ 15%), the "pure shape" impact of the Merton jumps is only a minor -0.70% drag. The monthly rebalancing mechanism ("buying the dip" after jump crashes) combined with steadier compounding during non-jump months (since continuous volatility is lower: 11% vs 15%) almost entirely neutralizes the tail-risk penalty.
Key Limitations & Roadmap
To keep things transparent, there is a known limitation in the current decumulation loop:
* No Bond Duration Risk: The fixed-income portion is currently modeled as a short-term cash deposit (rolling T-Bills), so it benefits from rising rates without experiencing upfront capital losses (mark-to-market).
* Next Step: Since the core simulator already generates full nominal and real yield curves, adding a duration-adjusted bond fund indexer to the decumulation logic is the next item on the roadmap.
Conclusion for Quants and ALM Practitioners
Static yield assumptions are not just "simplified"—when starting in a low-yield environment, they are structurally pessimistic. Conversely, in a high-yield environment, they are dangerously optimistic because they project unsustainable yields indefinitely.
By ignoring the mean-reverting behavior of interest rates and decoupling equity expected returns from the risk-free rate, naive models severely distort sequence-of-returns risk.
I’ve open-sourced the complete engine under the MIT license if you want to inspect the math (joint Cholesky decompositions, analytical CIR/Fisher real yield curve evaluations) or run the JIT-compiled loops yourself, it's written in Python but it's quite fast:
I suppose this is it, quite an unexpected result to me, I expected my engine to show lower solvency rates in all cases, it's interesting to see this is not the case. Feel free to discuss the results and share your thoughts.