Skip to main content

goal-based-allocation

Semi-analytical dynamic mean-variance allocation and terminal-wealth risk under regime-switching jump-diffusions

The package models two regimes and an absorbing wealth floor; it is not a discrete constrained multi-asset optimiser, trading engine, or production portfolio-management system.

Install: pip install goal-based-allocation · Import: goal_based_allocation · Status: Beta

PyPI Python CI Docs License Downloads Monthly

Documentation: goalbasedallocation.readthedocs.io

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.


Why goal-based-allocation

This package provides a semi-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 transforms are analytical; their inversion and the Riccati system are solved numerically.

It models two regimes and multi-asset mandates aggregated to one effective risky asset. It is not a discrete constrained multi-asset optimiser, trading engine, or production portfolio-management system; use optimalportfolios for rolling multi-asset construction, constraints, transaction costs, and backtesting.

The MV-optimal strategy takes the form

$$\omega^{\ast}(t) = |\omega^{\ast}_a| \cdot \left(\frac{\Pi^{\ast}(t)}{\Pi_t} - 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:

  1. Survived density — wealth above the floor at horizon, computed via Laplace inversion of the bounded transition density
  2. Floor atom — probability mass from diffusion paths hitting the absorbing barrier
  3. 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 differentiators

  • 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 independent validation of the semi-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. Analytical transforms and deterministic numerical inversion produce the results; Monte Carlo is 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) mu_growth, mu_stress
$\mu^{[i]} = \bar{\mu}^{[i]} - \lambda \alpha^{[i]}$ Diffusion drift mu_bar
$r_h = \max(r, c)$ Hurdle rate r_h
$r_c = r_h - c = \max(0, r-c)$ Net floor growth rate r_c
$\omega^{\ast}_a = -\tilde{a}^{[i]}/\Sigma^{[i]}$ Risky allocation coefficient w_a

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) = |\omega_a^{\ast}| \cdot (\Pi^{\ast}(t)/\Pi_t - 1)$, from Riccati system
Absorbing floor Wealth stopped at $L_t = L_0 \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) $\sigma_g$ / $\sigma_s$ $\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}[\Pi_T]$ 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 A.1).

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/
├── src/
│   └── goal_based_allocation/      # Core library
│       ├── regime_switch_paper.py  # 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
│       ├── vanilla_option_pricer.py # Regime-switching vanilla option pricing
│       └── run/                     # Source-only development runners (*_local.py)
├── examples/                       # Minimal, self-contained illustrations
│   ├── wealth_process_simulation.py
│   ├── terminal_wealth_distribution.py
│   ├── investment_opportunity_set.py
│   └── regime_switch_smile.py      # Vol smile + Fourier/MC reference pricers
├── papers/
│   └── goal_based_allocation_2026/
│       ├── paper/                    # Current LaTeX, PDF and approved figures
│       ├── replication/              # Producer, static inputs and paper tests
│       └── drafts/, presentations/, private/, agents/  # Local sections
├── pyproject.toml
├── LICENSE
└── README.md

Module reference

Module Description Key functions
regime_switch_paper Core Laplace framework for the RS-JD gap process compute_density, compute_survival, compute_tilted_survival, compute_overshoot_density, bh_moments_rsjd
riccati_solver Riccati ODE for MV-optimal policy, gap process, MC validation find_ell, gap_process_asset, simulate_mv_optimal
laplace_inversion Numerical Laplace inversion algorithms laplace_invert_abate_whitt, laplace_invert_stehfest
client_solver Effective single-asset from multi-asset portfolios build_effective_asset, portfolio_sigma_unc, portfolio_eta_quadrature
mandate_utils Named mandates (Income, Conservative, Balanced, Growth) mandate_effective_asset
opportunity_set Two-step advisor framework: opportunity set + client profile AdvisorSpec, compute_opportunity_point, build_opportunity_set
vanilla_option_pricer Laplace-transform vanilla option pricing under regime switching RiskNeutralParams, price_vanilla, implied_vol

Component development checks live under src/goal_based_allocation/run/ as <subject>_local.py. Each runner exposes Locals and run_local(local=...); these files are excluded from built distributions. Automated checks remain in the top-level tests/ directory.

Five-minute quickstart

The tested getting-started script runs offline from a source checkout and exercises the same public API used below:

python examples/getting_started/quickstart.py

1. Compute survival probability for a single asset

from goal_based_allocation import create_paper_assets, compute_survival

assets = create_paper_assets()
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 goal_based_allocation import (
    create_paper_assets, compute_density, compute_survival,
    compute_overshoot_density
)
from goal_based_allocation.riccati_solver import find_ell, gap_process_asset

assets = create_paper_assets()
eq = assets['equity']
T = 10.0

# Solve the Riccati system. target_return fixes the present value of the target,
# Pi*(0) = Pi0 * exp(0.04 * T); it is not the expected terminal wealth (see below).
ell, ric = find_ell(eq, T, target_return=0.04, r=0.02, c=0.02)
gap = gap_process_asset(ric)

# Terminal targets
PiT = ric.derived_at_tau(0)['Pi_star'][0]   # target wealth at T, equal to ell / 2
L_T = eq.pi_floor                            # floor at T (with r=c)
B_T = PiT - L_T                              # buffer

# Bounded gap density -> wealth density
x_grid = np.linspace(0.001, 4.0, 800)
d0, d1 = compute_density(T, x_grid, gap)
density_total = d0 + d1

# Survival and overshoot
S = compute_survival(T, gap.x0, gap)
d_ov = np.linspace(0.001, 8.0, 400)
f_ov = compute_overshoot_density(T, d_ov, gap)

print(f"target wealth = {PiT:.1f}, floor = {L_T:.1f}")
print(f"Survival = {S:.4f}")
print(f"Overshoot mass = {np.trapezoid(f_ov, d_ov):.4f}")
print(f"Floor atom = {1 - S - np.trapezoid(f_ov, d_ov):.4f}")

find_ell matches target_return to $-b(T)/(2a(T))$, the present value of the policy's target, $\Pi^{\ast}(0) = \Pi_0 e^{\rho T}$, not to the expected terminal wealth. Here $c = r$, so the target grows at $r_c = 0$ and equals $100 e^{0.4} \approx 149.2$ at both ends of the horizon, while the expected terminal wealth of the floor-protected strategy is about 106.4. The chapter The MV-optimal policy and the Riccati system of the handbook gives the expected terminal wealth in closed form and the multiplier ell for an expected-wealth target.

3. Compare MV-optimal vs buy-and-hold

from goal_based_allocation import (
    build_effective_asset, bh_moments_rsjd, compute_survival
)
from goal_based_allocation.riccati_solver import find_ell, gap_process_asset

# Build balanced mandate (35% bonds, 43% equity, 22% PE)
eff = build_effective_asset(w_eq=0.43, w_pe=0.22, k=3.0)
T, PI0 = 10.0, 100.0

# MV-optimal survival (analytical via Laplace); target_return sets Pi*(0), as above
ell, ric = find_ell(eff, T, target_return=0.04, r=0.02, c=0.0)
gap = gap_process_asset(ric)
S = compute_survival(T, gap.x0, gap)
print(f"MV survival: {S:.3f}")

# BH moments (exact via matrix exponential)
bh = bh_moments_rsjd(T, PI0, eff, c=0.0)
print(f"BH: E={bh['E']:.0f}, Std={bh['Std']:.1f}, r_impl={bh['r_impl']*100:.2f}%")

4. Build an investment opportunity set

from goal_based_allocation import AdvisorSpec, build_opportunity_set

spec = AdvisorSpec(omega_0=1.0, c=0.0, q=2/3, q_dd=2.0)
opp = build_opportunity_set(spec)

for p in opp:
    print(f"Bonds={p['w_bd']:4.0%}  r_impl={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 goal_based_allocation import RiskNeutralParams, price_vanilla, implied_vol, Regime

# risk-neutral parameters (rate convention: eta_rate = 1 / eta_mean)
params = RiskNeutralParams.from_rates(sigma_0=0.18, sigma_1=0.28,
                                      lambda_01=0.10, lambda_10=1.0,
                                      eta_01=3.0, eta_10=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, option_type='call')
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/regime_switch_smile.py for the smile plus Fourier and Monte Carlo cross-checks.

Reproducing Paper Results

The papers/ projects are available from a GitHub clone and are intentionally not included in PyPI distributions. From a clone, the main paper's integration tests and figures can be reproduced with the commands below. The current --test mode runs the tests and then generates all figures; use --outdir to keep verification output away from tracked figures.

# Run from the repository root; output defaults to the external local runtime.
python papers/goal_based_allocation_2026/replication/generate_paper_figures.py --test
python papers/goal_based_allocation_2026/replication/generate_paper_figures.py
python papers/goal_based_allocation_2026/replication/generate_paper_figures.py --figure 10

The main manuscript, PDF and approved figures now live in paper/; research code and static inputs live in replication/. See the paper index. An optional --outdir must be absolute and outside the checkout and OneDrive; it applies to both normal generation and --test.

Integration tests

The --test flag runs 7 tests with 9 assertions covering:

Test Description Tolerance
1 Unbounded density normalisation (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% opportunity_set_c0.png
2 Investment opportunity set, c=2.5% opportunity_set_c25.png
3 Expected allocation glide paths, c=0% allocation_paths_c0.png
4 Expected allocation glide paths, c=2.5% allocation_paths_c25.png
5 Allocation ±1σ uncertainty bands risky_allocation_subplots_c0.png
6 Path dynamics: survived vs stopped path_dynamics_balanced.png
7 Three wealth distributions: Growth mandate floor_vs_lipton.png
8 Three wealth distributions: Balanced mandate floor_vs_lipton_balanced.png
9 MV-optimal vs buy-and-hold density overlay (4 mandates) mandate_density_overlay_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 $X_t = \ln(B_t/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 (portfolio_eta_quadrature). Buy-and-hold benchmark moments are computed exactly via the 2×2 matrix exponential of Proposition A.1.

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. (2002). Assets with jumps. Risk, 15(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.

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
option-chain-analytics Point-in-time option-chain normalisation and reconstruction
trendfollowing Trend-following systems: closed-form theory and replication
privateassets Private-asset return analytics
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

goal-based-allocation is standalone within this stack: its runtime dependencies are NumPy, SciPy, and Matplotlib. The ArturSepp profile is the canonical ten-package catalogue.

Feedback & contributing

  • Report a reproducible bug, including the package version, Python/platform, parameters, and expected versus actual result.
  • Request a feature, explaining which replication result or parameterisation is unclear, the current workaround, and the smallest useful API.
  • Read CONTRIBUTING.md for development commands, numerical-validation rules, and pull-request guidance.

Citation

If you use this work in your research, please cite both the paper and the software. Machine-readable metadata is available in CITATION.cff.

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        = {goal-based-allocation: A {Python} package for dynamic mean-variance
                  portfolio allocation under regime-switching jump-diffusions},
  year         = {2026},
  version      = {0.4.1},
  howpublished = {\url{https://github.com/ArturSepp/GoalBasedAllocation}}
}

License

MIT — see LICENSE for details.

Metadata

Release files for goal-based-allocation 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for goal-based-allocation 0.4.1
File Size Uploaded
goal_based_allocation-0.4.1.tar.gz 66.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for goal-based-allocation 0.4.1
File Interpreter ABI Platform
goal_based_allocation-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 117.2 kB

Release files / goal_based_allocation-0.4.1.tar.gz

Download URL goal_based_allocation-0.4.1.tar.gz
Size 66.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f0959179e25f80114cafdb4da40356d385e4254f4869c03cde138a692424dd85
BLAKE2b-256 checksum
How to use checksums
1579c144bf962607502ea51418466c5afe1643c0a0211935fd489a53ef6637a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / goal_based_allocation-0.4.1-py3-none-any.whl

Download URL goal_based_allocation-0.4.1-py3-none-any.whl
Size 50.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d056680923c2541c34783455659bc68501d609cf4483df43199118234b0feb19
BLAKE2b-256 checksum
How to use checksums
82ad786ee6e4106b19e01740fb88a4996c9088bd83cfcc1b41578db9f077c597
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.2

2 release files

This release

0.4.1 This release

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

1 release file

0.1.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page