ArturSepp
GoalBasedAllocation
Python

Dynamic mean-variance allocation under regime-switching jump-diffusions with wealth floors - fully analytical (SSRN 6534579)

Last updated Aug 8, 2026
10
Stars
2
Forks
0
Issues
0
Stars/day
Attention Score
45
Language breakdown
Python 63.3%
TeX 36.7%
โ–ธ Files click to expand
README

GoalBasedAllocation (goal-based-allocation)

Dynamic Mean-Variance Portfolio Allocation under Regime-Switching Jump-Diffusions with Absorbing Barriers and Distribution Matching

PyPI Python License Tests Downloads Monthly

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
Laplace inversion of the bounded transition density
  • Floor atom โ€” probability mass from diffusion paths hitting the absorbing barrier
  • Jump-overshoot density โ€” wealth below the floor from crash jumps that gap
through the barrier, computed via the overshoot distribution

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
tactical asset allocation for multi-asset portfolios. Journal of Portfolio Management, 52(4), 86-120.
  • Sepp, A., Hansen, E., and Kastenholz, M. (2026). Capital market assumptions and strategic
asset allocation using multi-asset tradable factors. Under revision at the Journal of Portfolio Management.
  • Sepp, A. (2004). Analytical pricing of double-barrier options under a double-exponential
jump-diffusion process. International Journal of Theoretical and Applied Finance, 7(2), 151-175.
  • Sepp, A. (2006). Extended CreditGrades model with stochastic volatility and jumps.
Wilmott Magazine, September, 50-62.
  • 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
of jumps in asset prices. Mathematical Finance, 19(3), 379-401.
  • Abate, J. and Whitt, W. (1995). Numerical inversion of Laplace transforms of probability
distributions. ORSA Journal on Computing, 7(1), 36-43.

License

MIT โ€” see LICENSE for details.

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท ArturSepp/GoalBasedAllocation ยท Updated daily from GitHub