RiskAnalytics
Regime-conditional VaR, portfolio Greeks, stress testing
RiskAnalytics is v1’s advanced risk measurement module. It computes regime-conditional VaR (VaR that changes by market regime), portfolio Greeks aggregated across option positions, stress test scenarios, and correlation structure decomposition.
Import
from horizon.fund._risk_analytics import RiskAnalytics, VaRResult, StressTestResult
What it measures
Regime-conditional VaR
The key insight: a single VaR computed from all history averages across regimes and systematically underestimates crisis losses. RiskAnalytics detects the current regime and reports the VaR conditional on that regime.
API
class RiskAnalytics:
def __init__(
self,
confidence_level: float = 0.95,
regime_lookback: int = 252,
) -> None:
...
def compute_var(
self,
returns: list[float],
method: str = "historical", # "historical" | "parametric" | "monte_carlo"
regime: str | None = None,
) -> VaRResult:
...
def compute_portfolio_greeks(
self,
positions: list[OptionPosition],
spot_prices: dict[str, float],
) -> PortfolioGreeks:
...
def stress_test(
self,
positions: list[Position],
scenario: Scenario,
) -> StressTestResult:
...
def correlation_shock(
self,
positions: list[Position],
shock_correlation: float = 1.0,
) -> float:
"""Return portfolio vol if all correlations went to `shock_correlation`."""
Result types
@dataclass(frozen=True)
class VaRResult:
var_95: float
var_99: float
cvar_95: float # Expected shortfall
cvar_99: float
regime: str # "trending" | "mean_reverting" | "crisis"
method: str
n_observations: int
@dataclass(frozen=True)
class PortfolioGreeks:
delta: float # $ delta (directional)
gamma: float # $ gamma
vega: float # $ vega (vol exposure)
theta: float # $ theta / day
dollar_delta: float # delta * underlying price
dollar_gamma: float
@dataclass(frozen=True)
class StressTestResult:
scenario_name: str
pnl_impact_usd: float
pnl_impact_pct: float
positions_affected: list[str]
worst_position: str
Usage
from horizon.fund._risk_analytics import RiskAnalytics
analytics = RiskAnalytics(confidence_level=0.95, regime_lookback=252)
# Compute VaR from a return series
returns = [0.001, -0.002, 0.003, ...]
var = analytics.compute_var(returns, method="historical")
print(f"Regime: {var.regime}")
print(f"VaR 95%: {var.var_95:.3%}")
print(f"CVaR 95%: {var.cvar_95:.3%}")
print(f"VaR 99%: {var.var_99:.3%}")
# Portfolio Greeks (requires option positions)
positions = [
OptionPosition(symbol="AAPL", strike=180, expiry="2025-01-17", right="call", qty=10, delta=0.5, gamma=0.02, vega=0.3, theta=-0.05),
# ...
]
greeks = analytics.compute_portfolio_greeks(positions, spot_prices={"AAPL": 185})
print(f"Portfolio delta: ${greeks.dollar_delta:,.0f}")
print(f"Portfolio vega: ${greeks.vega:,.0f}")
print(f"Portfolio theta: ${greeks.theta:,.0f}/day")
# Correlation shock. "what if everything moves together"
shocked_vol = analytics.correlation_shock(positions, shock_correlation=1.0)
normal_vol = analytics.correlation_shock(positions, shock_correlation=0.3)
print(f"Normal vol: {normal_vol:.2%}")
print(f"Shocked vol: {shocked_vol:.2%}")
print(f"Shock amplification: {shocked_vol / normal_vol:.1f}×")
Integration with Horizon
Wire RiskAnalytics into your backtest to get regime-conditional risk metrics alongside the standard MetricsCollector:
import horizon as hz
from horizon.fund._risk_analytics import RiskAnalytics
analytics = RiskAnalytics()
result = hz.run(mode="backtest", strategies=[...], ...)
# Compute returns from equity curve
equities = [e for _, e in result.equity_curve]
returns = [equities[i] / equities[i-1] - 1 for i in range(1, len(equities))]
var = analytics.compute_var(returns)
print(f"Backtest regime: {var.regime}")
print(f"Historical VaR 95%: {var.var_95:.3%}")
print(f"Historical CVaR 95%: {var.cvar_95:.3%}")
For a risk overlay that automatically reduces exposure when VaR exceeds a threshold, wrap in a custom risk check (see Risk Management).
Stress scenarios
The v1 module ships with predefined scenarios you can apply to any position list:
When to use
Pitfalls
Source
python/horizon/fund/_risk_analytics.py. ~400 lines.