Dynamic mean-variance allocation under regime-switching jump-diffusions with wealth floors - fully analytical (SSRN 6534579)
GoalBasedAllocation (goal-based-allocation)
Dynamic Mean-Variance Portfolio Allocation under Regime-Switching Jump-Diffusions with Absorbing Barriers and Distribution Matching
Paper: companion code to Sepp, A. (2026), Dynamic Mean-Variance Portfolio Allocation under Regime-Switching Jump-Diffusions with Absorbing Barriers and Distribution Matching โ SSRN 6534579. See Citation for BibTeX.
Overview
This package provides a fully analytical Laplace-transform framework for dynamic mean-variance (MV) portfolio allocation under a two-state regime-switching model with exponential jumps at regime transitions and an absorbing wealth floor.
The MV-optimal strategy takes the form
$$\omega^{\ast}(t) = |\omega^{\ast}a| \cdot \left(\frac{\Pi^{\ast}(t)}{\Pit} - 1\right)$$
where $\Pi^{\ast}(t)$ is the target wealth trajectory derived from the Riccati ODE system, and $|\omega^{\ast}_a|$ is the regime-dependent allocation intensity. This produces an endogenous de-risking glide path: early in the horizon the funding gap is large and allocation is aggressive; as the portfolio approaches the target, allocation moderates automatically.
The terminal wealth density has three analytically tractable components:
- Survived density โ wealth above the floor at horizon, computed via
- Floor atom โ probability mass from diffusion paths hitting the absorbing barrier
- Jump-overshoot density โ wealth below the floor from crash jumps that gap
All three components are computed semi-analytically using the Abate-Whitt (1995) Euler acceleration method for numerical Laplace inversion. No Monte Carlo simulation is needed for pricing; MC is used only for validation.
Key features
- Analytical survival probability, conditional moments, and tilted survival via Laplace transforms
- Riccati ODE system for the MV-optimal policy with regime-dependent coefficients
- Full terminal wealth density decomposition (survived + floor atom + overshoot)
- Exact buy-and-hold moments under regime-switching via 2ร2 matrix exponential
- Portfolio mandate construction with deterministic quadrature for jump aggregation
- Expected allocation glide paths with variance bands
- Investment opportunity set construction for client-facing portfolio advice
- Vanilla option pricing under the same regime-switching jump-diffusion via one Laplace inversion (calls/puts, both regimes, joint strikes)
- Monte Carlo simulator for validation of all analytical results
- Integration tests and all paper figures reproducible from a single command
When to use it โ and when not
Use goal-based-allocation to design goal-based mandates under regime switching: survival and shortfall probabilities against a wealth floor, MV-optimal allocation intensities and glide paths, terminal wealth distributions, investment opportunity sets for client advice, and regime-switching vanilla option pricing on the same Laplace engine โ all analytically, with Monte Carlo reserved for validation.
The model is deliberately parsimonious: two regimes, exponential jumps at transitions, and multi-asset mandates reduced to a single effective asset via portfolio aggregation. For discrete rolling multi-asset optimisation with weight constraints and transaction costs, use optimalportfolios.
Model
The portfolio wealth $\Pi_t$ follows a regime-switching jump-diffusion with two states: growth ($i=1$) and stress ($i=2$).
Notation (paper โ code)
| Symbol | Name | Code variable | |---|---|---| | $\bar{\mu}^{[i]}$ | Total expected return (CMA observable) | mugrowth, mustress | | $\mu^{[i]} = \bar{\mu}^{[i]} - \lambda \alpha^{[i]}$ | Diffusion drift | mu_bar | | $rh = \max(r, c)$ | Hurdle rate | rh | | $rc = rh - c = \max(0, r-c)$ | Net floor growth rate | r_c | | $\omega^{\ast}a = -\tilde{a}^{[i]}/\Sigma^{[i]}$ | Risky allocation coefficient | wa |
Process specification
| Component | Description | |---|---| | Diffusion | Regime-dependent drift $\mu^{[i]}$ and volatility $\sigma^{[i]}$ | | Jumps | Exponential crash at growthโstress ($\eta^{[1]}$), exponential recovery at stressโgrowth ($\eta^{[2]}$) | | Regime switching | Poisson rates $\lambda^{[12]}$ (crash) and $\lambda^{[21]}$ (recovery) | | MV-optimal control | $\omega^{\ast}(t) = \|\omegaa^{\ast}\| \cdot (\Pi^{\ast}(t)/\Pit - 1)$, from Riccati system | | Absorbing floor | Wealth stopped at $Lt = L0 \cdot e^{r_c t}$, converting to cash |
Asset class parameters
| | Bonds | Equity | Private Equity | |---|---|---|---| | $\bar{\mu}^{[1]}$ / $\bar{\mu}^{[2]}$ | 2.5% / 2.0% | 4.5% / 0.0% | 7.0% / 0.0% | | $\sigma^{[1]}$ / $\sigma^{[2]}$ | 6% / 9% | 15% / 22.5% | 20% / 30% | | Crash loss / Recovery gain | 8% / 5% | 25% / 15% | 30% / 20% |
Shared: $\lambda^{[12]} = 0.1$, $\lambda^{[21]} = 1.0$, $r = 2\%$, $T = 10$y, $\Pi_0 = 100$.
Mandate specification
Mandates are defined by bond weight $w$ with equity-PE split $g = 2/3$: $w{\text{eq}} = g(1-w)$, $w{\text{pe}} = (1-g)(1-w)$.
| Mandate | Weights (Bd/Eq/PE) | $\sigmag$ / $\sigmas$ | $\bar{\mu}g$ / $\bar{\mu}s$ | $\eta^{[1]}$ | |---|---|---|---|---| | Income | 100/0/0 | 6.0 / 9.0% | 2.50 / 2.00% | 0.087 | | Conservative | 65/23/12 | 7.7 / 11.6% | 3.49 / 1.30% | 0.161 | | Balanced | 35/43/22 | 11.1 / 16.7% | 4.34 / 0.70% | 0.233 | | Growth | 0/67/33 | 15.8 / 23.8% | 5.33 / 0.00% | 0.334 |
MV-optimal results ($c=0\%$, $\omega^{\ast}(0)=1$, $q_{dd}=2$)
| Mandate | $r{\text{impl}}$ | $\Pi^{\ast}T$ | $\mathbb{E}[\PiT]$ | Std | Survival | $r^{\text{BH}}{\text{impl}}$ | $\text{Std}^{\text{BH}}$ | |---|---|---|---|---|---|---|---| | Income | 2.39% | 240 | 127 | 24.7 | 85.5% | 2.46% | 30.4 | | Conservative | 2.92% | 200 | 134 | 33.7 | 83.2% | 3.33% | 48.3 | | Balanced | 3.30% | 221 | 139 | 46.1 | 78.7% | 4.10% | 74.8 | | Growth | 3.68% | 254 | 144 | 61.9 | 73.8% | 5.01% | 117.6 |
Floor protection cost ranges from 6bp (income) to 117bp (growth). BH moments are exact under the RS-JD model via 2ร2 matrix exponential (Proposition B.7).
Installation
# From PyPI
pip install goal-based-allocation
Or directly from GitHub
pip install git+https://github.com/ArturSepp/GoalBasedAllocation.git
Or clone and install in development mode
git clone https://github.com/ArturSepp/GoalBasedAllocation.git
cd GoalBasedAllocation
pip install -e .
Requires Python >= 3.10 with NumPy >= 1.24, SciPy >= 1.10, and Matplotlib >= 3.7.
Package Structure
GoalBasedAllocation/
โโโ goalbasedallocation/ # Core library
โ โโโ regimeswitchpaper.py # Laplace framework: density, survival, overshoot, BH moments
โ โโโ riccati_solver.py # Riccati ODE system + MC simulator
โ โโโ laplace_inversion.py # Abate-Whitt & Stehfest numerical inversion
โ โโโ client_solver.py # Effective asset construction, portfolio eta quadrature
โ โโโ mandate_utils.py # Portfolio mandate construction from assets
โ โโโ opportunity_set.py # Investment opportunity set & advisor framework
โ โโโ vanillaoptionpricer.py # Laplace-transform vanilla option pricing (regime switching)
โโโ examples/ # Minimal, self-contained illustrations
โ โโโ wealthprocesssimulation.py
โ โโโ terminalwealthdistribution.py
โ โโโ investmentopportunityset.py
โ โโโ regimeswitchsmile.py # Vol smile + Fourier/MC reference pricers
โโโ paper_code/
โ โโโ goalbasedallocation_2026/ # Self-contained paper: LaTeX, PDF, figures
โ โโโ goalbasedallocation_2026.tex
โ โโโ generatepaperfigures.py # All 10 figures + integration tests
โ โโโ figures/
โโโ pyproject.toml
โโโ LICENSE
โโโ README.md
Module reference
| Module | Description | Key functions | |---|---|---| | regimeswitchpaper | Core Laplace framework for the RS-JD gap process | computedensity, computesurvival, computetiltedsurvival, computeovershootdensity, bhmomentsrsjd | | riccatisolver | Riccati ODE for MV-optimal policy, gap process, MC validation | findell, gapprocessasset, simulatemvoptimal | | laplaceinversion | Numerical Laplace inversion algorithms | laplaceinvertabatewhitt, laplaceinvertstehfest | | clientsolver | Effective single-asset from multi-asset portfolios | buildeffectiveasset, portfoliosigmaunc, portfolioeta_quadrature | | mandateutils | Named mandates (Income, Conservative, Balanced, Growth) | mandateeffective_asset | | opportunityset | Two-step advisor framework: opportunity set + client profile | AdvisorSpec, computeopportunitypoint, buildopportunity_set | | vanillaoptionpricer | Laplace-transform vanilla option pricing under regime switching | RiskNeutralParams, pricevanilla, impliedvol |
Quick Start
1. Compute survival probability for a single asset
from goalbasedallocation import createpaperassets, compute_survival
assets = createpaperassets() eq = assets['equity']
for T in [1, 2, 5, 10]: S = compute_survival(T, eq.x0, eq) print(f"T={T:2d}y: survival={S:.4f}, stopping={1-S:.4f}")
2. Solve the MV-optimal allocation and compute the terminal wealth density
import numpy as np
from goalbasedallocation import (
createpaperassets, computedensity, computesurvival,
computeovershootdensity
)
from goalbasedallocation.riccatisolver import findell, gapprocessasset
assets = createpaperassets() eq = assets['equity'] T = 10.0
Solve Riccati ODE for target return of 4%
ell, ric = findell(eq, T, targetreturn=0.04, r=0.02, c=0.02)
gap = gapprocessasset(ric)
Terminal targets
PiT = ric.derivedattau(0)['Pi_star'][0] # target wealth at T
LT = eq.pifloor # floor at T (with r=c)
BT = PiT - LT # buffer
Bounded gap density -> wealth density
x_grid = np.linspace(0.001, 4.0, 800)
d0, d1 = computedensity(T, xgrid, gap)
density_total = d0 + d1
Survival and overshoot
S = compute_survival(T, gap.x0, gap)
d_ov = np.linspace(0.001, 8.0, 400)
fov = computeovershootdensity(T, dov, gap)
print(f"target wealth = {PiT:.1f}, floor = {L_T:.1f}") print(f"Survival = {S:.4f}") print(f"Overshoot mass = {np.trapezoid(fov, dov):.4f}") print(f"Floor atom = {1 - S - np.trapezoid(fov, dov):.4f}")
3. Compare MV-optimal vs buy-and-hold
from goalbasedallocation import (
buildeffectiveasset, bhmomentsrsjd, compute_survival
)
from goalbasedallocation.riccatisolver import findell, gapprocessasset
Build balanced mandate (35% bonds, 43% equity, 22% PE)
eff = buildeffectiveasset(weq=0.43, wpe=0.22, k=3.0)
T, PI0 = 10.0, 100.0
MV-optimal survival (analytical via Laplace)
ell, ric = findell(eff, T, targetreturn=0.04, r=0.02, c=0.0)
gap = gapprocessasset(ric)
S = compute_survival(T, gap.x0, gap)
print(f"MV survival: {S:.3f}")
BH moments (exact via matrix exponential)
bh = bhmomentsrsjd(T, PI0, eff, c=0.0)
print(f"BH: E={bh['E']:.0f}, Std={bh['Std']:.1f}, rimpl={bh['rimpl']*100:.2f}%")
4. Build an investment opportunity set
from goalbasedallocation import AdvisorSpec, buildopportunityset
spec = AdvisorSpec(omega0=1.0, c=0.0, q=2/3, qdd=2.0) opp = buildopportunityset(spec)
for p in opp: print(f"Bonds={p['wbd']:4.0%} rimpl={p['r_impl']:5.2%} " f"Surv={p['S']:5.1%} Median={p['q50']:6.0f}")
5. Price a vanilla option under regime switching
import numpy as np
from goalbasedallocation import RiskNeutralParams, pricevanilla, impliedvol, Regime
risk-neutral parameters (rate convention: etarate = 1 / etamean)
params = RiskNeutralParams.fromrates(sigma0=0.18, sigma_1=0.28,
lambda01=0.10, lambda10=1.0,
eta01=3.0, eta10=8.0, rate=0.03)
strikes = np.array([80.0, 100.0, 120.0, 150.0, 200.0]) calls = price_vanilla(params, spot=100.0, strikes=strikes, ttm=10.0, regime=Regime.GROWTH, opti) vols = implied_vol(params, spot=100.0, strikes=strikes, ttm=10.0, regime=Regime.GROWTH)
for k, c, v in zip(strikes, calls, vols): print(f"K={k:5.0f} call={c:8.4f} implied_vol={v:6.2%}")
Prices come from a single Laplace inversion in maturity (Abate-Whitt), so all strikes are priced jointly and the same characteristic roots reused by the wealth-floor machinery. See examples/regimeswitchsmile.py for the smile plus Fourier and Monte Carlo cross-checks.
Reproducing Paper Results
All figures and integration tests can be reproduced with a single command:
cd papercode/goalbasedallocation2026
Run integration tests (9 assertions)
python generatepaperfigures.py --test
Generate all figures only (writes to ./figures/)
python generatepaperfigures.py
Generate a single figure
python generatepaperfigures.py --figure 10
Custom output directory
python generatepaperfigures.py --outdir my_figures/
Integration tests
The --test flag runs 7 tests with 9 assertions covering:
| Test | Description | Tolerance | |---|---|---| | 1 | Unbounded density normalization (with and without jumps) | < 1e-4 | | 2 | Barrier density vs analytical survival consistency | < 1e-4 | | 3 | Survival probability monotonicity across horizons | -- | | 4 | Three-asset survival comparison | -- | | 5 | Riccati ODE initial conditions a(0) = [1, 1] | < 1e-6 | | 6 | Gap-process survival: analytical vs MC (100K paths) | < 5 pp | | 7 | Table 1 parameter validation (3 assets) | exact |
Figure catalog
| # | Description | File | |---|---|---| | 1 | Investment opportunity set, c=0% | opportunitysetc0.png | | 2 | Investment opportunity set, c=2.5% | opportunitysetc25.png | | 3 | Expected allocation glide paths, c=0% | allocationpathsc0.png | | 4 | Expected allocation glide paths, c=2.5% | allocationpathsc25.png | | 5 | Allocation ยฑ1ฯ uncertainty bands | riskyallocationsubplots_c0.png | | 6 | Path dynamics: survived vs stopped | pathdynamicsbalanced.png | | 7 | Three wealth distributions: Growth mandate | floorvslipton.png | | 8 | Three wealth distributions: Balanced mandate | floorvslipton_balanced.png | | 9 | MV-optimal vs buy-and-hold density overlay (4 mandates) | mandatedensityoverlay_c0.png | | 10 | Mandate comparison: analytical vs MC (3 mandates) | mandate_comparison.png |
Selected figures
Left: MV-optimal vs buy-and-hold terminal wealth densities for four mandates. Right: Analytical density (survived + overshoot) vs MC histograms.
Left: Survived and stopped sample paths with MV-optimal allocation. Right: Investment opportunity set with CDF quantiles.
Methodology
The analytical framework proceeds in three steps:
Step 1: Riccati ODE system. The pre-commitment MV problem reduces to a system of coupled Riccati ODEs for the value function coefficients in each regime. The solution yields the optimal allocation intensity $|\omega^{\ast}_a|$ and the target wealth trajectory $\Pi^{\ast}(t)$.
Step 2: Gap process. Under the optimal policy, the log-cushion ratio $Xt = \ln(Bt/Z_t)$ is a regime-switching jump-diffusion with an absorbing barrier at zero. Its Laplace-domain transition density satisfies a degree-6 characteristic polynomial with three positive and three negative roots.
Step 3: Laplace inversion. The bounded transition density, survival probability, tilted survival moments, and overshoot density are all expressed as Laplace transforms and inverted numerically using the Abate-Whitt (1995) Euler acceleration algorithm.
Portfolio aggregation. Multi-asset mandates are reduced to a single effective asset using portfolio volatility with full correlation structure, and portfolio jump sizes via deterministic numerical integration (portfolioetaquadrature). Buy-and-hold benchmark moments are computed exactly via the 2ร2 matrix exponential of Proposition B.7.
Ecosystem
This package is part of an open-source Python stack for quantitative finance โ full catalogue at github.com/ArturSepp:
| Package | Purpose | |---|---| | qis | Performance analytics, factsheets, and visualisation | | optimalportfolios | Portfolio construction and backtesting | | factorlasso | Sparse factor models and factor covariance estimation | | bbg-fetch | Bloomberg data fetching | | trendfollowing | Trend-following systems: closed-form theory and replication | | goal-based-allocation (this package) | Dynamic MV allocation under regime-switching jump-diffusions | | stochvolmodels | Stochastic volatility pricing analytics | | vanilla-option-pricers | Vectorised vanilla option pricers and implied volatility fitters |
Dependency links within the stack: optimalportfolios builds on qis and factorlasso; trendfollowing builds on qis.
Citation
If you use this work in your research, please cite both the paper and the software.
Paper:
@article{Sepp2026GoalBased,
author = {Sepp, Artur},
title = {Dynamic Mean-Variance Portfolio Allocation under Regime-Switching
Jump-Diffusions with Absorbing Barriers and Distribution Matching},
year = {2026},
note = {Available at SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6534579}
}
Software:
@misc{SeppGBA2026,
author = {Sepp, Artur},
title = {{GoalBasedAllocation}: A {Python} package for dynamic mean-variance
portfolio allocation under regime-switching jump-diffusions},
year = {2026},
note = {Version 0.2.0},
howpublished = {\url{https://github.com/ArturSepp/GoalBasedAllocation}}
}
Key References
- Sepp, A., Ossa, I., and Kastenholz, M. (2026). Robust optimization of strategic and
- Sepp, A., Hansen, E., and Kastenholz, M. (2026). Capital market assumptions and strategic
- Sepp, A. (2004). Analytical pricing of double-barrier options under a double-exponential
- Sepp, A. (2006). Extended CreditGrades model with stochastic volatility and jumps.
- Lipton, A. (2001). Mathematical Methods for Foreign Exchange. World Scientific.
- Lipton, A. (2001). Assets with jumps. Risk, 14(9), 149-153.
- Cont, R. and Tankov, P. (2009). Constant proportion portfolio insurance in the presence
- Abate, J. and Whitt, W. (1995). Numerical inversion of Laplace transforms of probability
License
MIT โ see LICENSE for details.