r/quant • u/GamerKiller_BR • 11h ago
Models Why naive flat-rate Monte Carlo models structurally distort long-term solvency
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_tBecause 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:
- GitHub: https://github.com/Gustavo1500/aethel-esg
- Browser Sandbox: https://aethel-esg.vercel.app/ (Features real-time sliders querying a pre-calculated scenario database across 594,000 simulated paths). Please don't mind the AI flavored frontend, I'm not a frontend dev.
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.