docs: Comprehensive enhancement of optimal control and HMM API documentation
Optimal Control: 94 to 610 lines with HJB theory, viscosity solutions, finite differences HMM API: 16 to 571 lines with complete API reference and usage examples
This commit is contained in:
@@ -1,76 +1,333 @@
|
||||
# Optimal Control
|
||||
|
||||
Hamilton–Jacobi–Bellman (HJB) solvers, regime-switching thresholds, OU parameter estimation, and Kalman filtering utilities backed by Rust.
|
||||
This module provides advanced optimal control algorithms for financial applications, including Hamilton-Jacobi-Bellman (HJB) equation solvers, regime-switching models, parameter estimation, and state-space filtering. All algorithms are implemented in high-performance Rust with Python bindings.
|
||||
|
||||
## HJB switching boundaries (OU process)
|
||||
## Mathematical Foundations
|
||||
|
||||
### Hamilton-Jacobi-Bellman (HJB) Equation
|
||||
|
||||
The HJB equation is a fundamental result in optimal control theory that provides the necessary and sufficient conditions for optimality of a control policy. For a stochastic control problem:
|
||||
|
||||
$$
|
||||
V(x) = \sup_{\alpha \in \mathcal{A}} \mathbb{E}\left[\int_0^\infty e^{-\rho t} L(X_t, \alpha_t) dt \mid X_0 = x\right]
|
||||
$$
|
||||
|
||||
where $V(x)$ is the value function, $\rho$ is the discount rate, $L$ is the running cost, and $X_t$ follows a controlled stochastic process. The HJB equation is:
|
||||
|
||||
$$
|
||||
\rho V(x) = \sup_{\alpha \in \mathcal{A}} \left\{ \mathcal{L}^\alpha V(x) + L(x, \alpha) \right\}
|
||||
$$
|
||||
|
||||
where $\mathcal{L}^\alpha$ is the infinitesimal generator of the controlled process.
|
||||
|
||||
#### Application to Mean-Reverting Spreads
|
||||
|
||||
For pairs trading with an Ornstein-Uhlenbeck (OU) spread process:
|
||||
|
||||
$$
|
||||
dX_t = \kappa(\theta - X_t)dt + \sigma dW_t
|
||||
$$
|
||||
|
||||
with transaction costs $c > 0$, the HJB equation becomes:
|
||||
|
||||
$$
|
||||
\rho V(x) = \kappa(\theta - x)V'(x) + \frac{\sigma^2}{2}V''(x) + \sup_{\alpha \in \{-1, 0, 1\}} \{ -c|\alpha| + \alpha x \}
|
||||
$$
|
||||
|
||||
The optimal control is a threshold policy: buy when $x < x_L$, sell when $x > x_U$, hold otherwise.
|
||||
|
||||
### Viscosity Solutions
|
||||
|
||||
Classical solutions to HJB equations rarely exist due to:
|
||||
1. **Non-smoothness at boundaries**: The value function $V(x)$ has kinks where the optimal control switches
|
||||
2. **Lack of regularity**: Second derivatives $V''(x)$ may not exist everywhere
|
||||
3. **Free boundary problems**: The optimal switching thresholds $(x_L, x_U)$ are unknown
|
||||
|
||||
**Viscosity solutions** generalize the notion of solution to allow for non-smooth value functions. A function $V$ is a viscosity solution if:
|
||||
|
||||
1. **Subsolution property**: For any smooth test function $\phi$ such that $V - \phi$ has a local maximum at $x_0$:
|
||||
$$\rho V(x_0) \leq \mathcal{H}(x_0, V(x_0), D\phi(x_0), D^2\phi(x_0))$$
|
||||
|
||||
2. **Supersolution property**: For any smooth test function $\psi$ such that $V - \psi$ has a local minimum at $x_0$:
|
||||
$$\rho V(x_0) \geq \mathcal{H}(x_0, V(x_0), D\psi(x_0), D^2\psi(x_0))$$
|
||||
|
||||
where $\mathcal{H}$ is the Hamiltonian.
|
||||
|
||||
**Key properties:**
|
||||
- **Uniqueness**: Under suitable conditions (coercivity, proper discount), the viscosity solution is unique
|
||||
- **Stability**: Viscosity solutions are stable under uniform convergence
|
||||
- **Numerical convergence**: Monotone finite difference schemes converge to the viscosity solution
|
||||
|
||||
### Finite Difference Methods
|
||||
|
||||
We discretize the HJB equation on a spatial grid $x_i = x_{\min} + ih$, $i = 0, \ldots, N$, with grid spacing $h$.
|
||||
|
||||
#### Upwind Schemes
|
||||
|
||||
For the OU drift term $\kappa(\theta - x)V'(x)$, we use **upwind finite differences** to ensure monotonicity and stability:
|
||||
|
||||
- If $\kappa(\theta - x_i) > 0$ (rightward drift): use forward difference
|
||||
$$V'(x_i) \approx \frac{V_{i+1} - V_i}{h}$$
|
||||
|
||||
- If $\kappa(\theta - x_i) < 0$ (leftward drift): use backward difference
|
||||
$$V'(x_i) \approx \frac{V_i - V_{i-1}}{h}$$
|
||||
|
||||
The diffusion term uses centered differences:
|
||||
$$V''(x_i) \approx \frac{V_{i+1} - 2V_i + V_{i-1}}{h^2}$$
|
||||
|
||||
#### Policy Iteration Algorithm
|
||||
|
||||
The HJB equation with control is solved via **policy iteration**:
|
||||
|
||||
1. **Initialize**: Start with policy $\alpha^{(0)}$ (e.g., always hold)
|
||||
2. **Policy evaluation**: Solve the linear system for value function $V^{(k)}$:
|
||||
$$\rho V^{(k)}_i = \mathcal{L}^{\alpha^{(k)}} V^{(k)}_i + L(x_i, \alpha^{(k)}_i)$$
|
||||
3. **Policy improvement**: Update policy by maximizing Hamiltonian:
|
||||
$$\alpha^{(k+1)}_i = \arg\max_{\alpha} \{ \mathcal{L}^\alpha V^{(k)}_i + L(x_i, \alpha) \}$$
|
||||
4. **Convergence check**: If $\|\alpha^{(k+1)} - \alpha^{(k)}\|_\infty < \epsilon$, stop; otherwise return to step 2
|
||||
|
||||
**Convergence properties:**
|
||||
- Typically 10-50 iterations for practical problems
|
||||
- Geometric convergence rate
|
||||
- Numerical solution converges to viscosity solution as $h \to 0$
|
||||
|
||||
## Implemented Algorithms
|
||||
|
||||
### 1. HJB Solver for OU Process
|
||||
|
||||
Solves the optimal switching problem for mean-reverting spreads with transaction costs.
|
||||
|
||||
**Implementation**: `src/optimal_control/hjb_solver.rs`
|
||||
|
||||
**Python API**:
|
||||
```python
|
||||
from optimizr import solve_hjb_py, solve_hjb_full_py
|
||||
|
||||
# Basic solver - returns optimal thresholds
|
||||
lower, upper, residual, iters = solve_hjb_py(
|
||||
kappa=3.0,
|
||||
theta=0.0,
|
||||
sigma=0.2,
|
||||
rho=0.04,
|
||||
transaction_cost=0.001,
|
||||
n_points=400,
|
||||
max_iter=4000,
|
||||
tolerance=1e-7,
|
||||
n_std=5.0,
|
||||
kappa=3.0, # Mean reversion speed
|
||||
theta=0.0, # Long-run mean
|
||||
sigma=0.2, # Volatility
|
||||
rho=0.04, # Discount rate
|
||||
transaction_cost=0.001, # Transaction cost per trade
|
||||
n_points=400, # Number of grid points
|
||||
max_iter=4000, # Maximum policy iterations
|
||||
tolerance=1e-7, # Convergence tolerance
|
||||
n_std=5.0, # Grid extent in standard deviations
|
||||
)
|
||||
print(f"bounds=({lower:.3f}, {upper:.3f}), residual={residual:.2e}, iters={iters}")
|
||||
|
||||
print(f"Optimal bounds: ({lower:.3f}, {upper:.3f})")
|
||||
print(f"Residual: {residual:.2e}, Iterations: {iters}")
|
||||
|
||||
# Full solver - also returns value function and derivatives
|
||||
lower, upper, residual, iters, V, V_x, V_xx = solve_hjb_full_py(
|
||||
kappa=3.0, theta=0.0, sigma=0.2, rho=0.04,
|
||||
transaction_cost=0.001, n_points=400
|
||||
)
|
||||
|
||||
# Plot value function derivatives for diagnostics
|
||||
import matplotlib.pyplot as plt
|
||||
plt.plot(V_x)
|
||||
plt.axvline(lower, color='r', linestyle='--', label='Lower bound')
|
||||
plt.axvline(upper, color='g', linestyle='--', label='Upper bound')
|
||||
plt.legend()
|
||||
plt.title("Value function derivative V'(x)")
|
||||
```
|
||||
|
||||
- Model: $dX_t = \kappa(\theta - X_t)\,dt + \sigma\,dW_t$ with quadratic transaction costs.
|
||||
- Output: optimal buy/sell thresholds; `solve_hjb_full_py` also returns $V, V_x, V_{xx}$ for diagnostics.
|
||||
- Diagnostics: plot $V_x$ for smoothness near thresholds; monitor `residual` and increase `max_iter` if not converged.
|
||||
**Parameters**:
|
||||
- `kappa`: Mean reversion speed (typical range: 0.1-10). Higher values → faster reversion → narrower bands
|
||||
- `theta`: Long-run mean (typically 0 for normalized spreads)
|
||||
- `sigma`: Volatility (typical range: 0.1-1.0). Higher values → wider bands
|
||||
- `rho`: Discount rate (typical: 0.01-0.1). Higher values → more myopic strategy
|
||||
- `transaction_cost`: Per-trade cost (typical: 0.0001-0.01). Higher values → wider bands, fewer trades
|
||||
- `n_points`: Grid resolution (recommended: 200-500). Higher → more accurate but slower
|
||||
- `n_std`: Grid extent (recommended: 3-6). Should cover 99%+ of spread distribution
|
||||
|
||||
## Backtesting optimal switching
|
||||
|
||||
```python
|
||||
from optimizr import backtest_optimal_switching_py
|
||||
|
||||
metrics = backtest_optimal_switching_py(
|
||||
spread=spread,
|
||||
lower_bound=lower,
|
||||
upper_bound=upper,
|
||||
transaction_cost=0.001,
|
||||
)
|
||||
(
|
||||
total_return,
|
||||
sharpe,
|
||||
max_dd,
|
||||
n_trades,
|
||||
win_rate,
|
||||
pnl_path,
|
||||
) = metrics
|
||||
```
|
||||
|
||||
Inspect `win_rate` vs `max_dd` to tune aggressiveness; combine with HMM regimes for state-aware controls.
|
||||
|
||||
## OU parameter estimation
|
||||
**Returns**:
|
||||
- `lower`: Optimal buy threshold (negative value)
|
||||
- `upper`: Optimal sell threshold (positive value)
|
||||
- `residual`: Maximum policy change in last iteration (should be < tolerance)
|
||||
- `iters`: Number of policy iterations (typically 10-50)
|
||||
- `V`, `V_x`, `V_xx`: (full solver only) Value function and derivatives on grid
|
||||
|
||||
**When to use**:
|
||||
- Pairs trading with mean-reverting spreads
|
||||
- Statistical arbitrage with transaction costs
|
||||
- Optimal entry/exit for mean-reverting assets
|
||||
- Requires reliable OU parameter estimates (see OU estimation below)
|
||||
|
||||
**Diagnostics**:
|
||||
- Plot $V'(x)$ to check smoothness near thresholds
|
||||
- Verify `residual < tolerance` for convergence
|
||||
- Check that thresholds are within grid bounds
|
||||
- If not converged: increase `max_iter` or adjust grid parameters
|
||||
|
||||
### 2. Viscosity Solution Solver
|
||||
|
||||
General-purpose viscosity solution solver for HJB equations with arbitrary Hamiltonians.
|
||||
|
||||
**Implementation**: `src/optimal_control/viscosity.rs`
|
||||
|
||||
**Usage**: Advanced users can extend this for custom control problems beyond OU switching.
|
||||
|
||||
### 3. Regime Switching Models
|
||||
|
||||
Optimal control with multiple market regimes, each with different dynamics.
|
||||
|
||||
**Implementation**: `src/optimal_control/regime_switching.rs`
|
||||
|
||||
**Approach**:
|
||||
1. Use HMM to identify hidden regimes (see HMM section)
|
||||
2. Estimate OU parameters per regime
|
||||
3. Solve HJB per regime to get regime-specific thresholds
|
||||
4. Switch control policy based on decoded regime
|
||||
|
||||
**Example workflow**:
|
||||
```python
|
||||
from optimizr import HMM, estimate_ou_params_py, solve_hjb_py
|
||||
import numpy as np
|
||||
|
||||
# Step 1: Train HMM on spread returns
|
||||
returns = np.diff(spread)
|
||||
hmm = HMM(n_states=2)
|
||||
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
|
||||
regimes = hmm.predict(returns.reshape(-1, 1))
|
||||
|
||||
# Step 2: Estimate OU parameters per regime
|
||||
params = []
|
||||
for regime_id in range(2):
|
||||
mask = (regimes == regime_id)
|
||||
spread_regime = spread[1:][mask] # Align with returns
|
||||
kappa, theta, sigma, half_life = estimate_ou_params_py(
|
||||
spread_regime, dt=1/252
|
||||
)
|
||||
params.append((kappa, theta, sigma))
|
||||
print(f"Regime {regime_id}: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
|
||||
|
||||
# Step 3: Solve HJB per regime
|
||||
thresholds = []
|
||||
for kappa, theta, sigma in params:
|
||||
lower, upper, _, _ = solve_hjb_py(
|
||||
kappa=kappa, theta=theta, sigma=sigma,
|
||||
rho=0.04, transaction_cost=0.001
|
||||
)
|
||||
thresholds.append((lower, upper))
|
||||
print(f"Thresholds: ({lower:.3f}, {upper:.3f})")
|
||||
|
||||
# Step 4: Apply regime-specific control
|
||||
current_regime = regimes[-1]
|
||||
lower, upper = thresholds[current_regime]
|
||||
if spread[-1] < lower:
|
||||
action = "BUY"
|
||||
elif spread[-1] > upper:
|
||||
action = "SELL"
|
||||
else:
|
||||
action = "HOLD"
|
||||
print(f"Current regime: {current_regime}, Action: {action}")
|
||||
```
|
||||
|
||||
### 4. Jump Diffusion Models
|
||||
|
||||
Extension of OU process with Poisson jumps for modeling sudden price shocks.
|
||||
|
||||
**Implementation**: `src/optimal_control/jump_diffusion.rs`
|
||||
|
||||
**Model**:
|
||||
$$
|
||||
dX_t = \kappa(\theta - X_t)dt + \sigma dW_t + J_t dN_t
|
||||
$$
|
||||
|
||||
where $N_t$ is a Poisson process with intensity $\lambda$, and $J_t \sim \mathcal{N}(\mu_J, \sigma_J^2)$ are jump sizes.
|
||||
|
||||
**Use case**: Markets with flash crashes, earnings announcements, or other discontinuous events.
|
||||
|
||||
### 5. Multi-Regime Switching Jump Diffusion (MRSJD)
|
||||
|
||||
Combines regime switching with jump diffusion for maximum flexibility.
|
||||
|
||||
**Implementation**: `src/optimal_control/mrsjd.rs`
|
||||
|
||||
**Model**: Each regime has its own OU parameters AND jump process parameters.
|
||||
|
||||
**Use case**: Complex markets with both regime changes and sudden shocks (e.g., crypto, emerging markets).
|
||||
|
||||
### 6. OU Parameter Estimation
|
||||
|
||||
Estimates Ornstein-Uhlenbeck process parameters from time series data.
|
||||
|
||||
**Implementation**: `src/optimal_control/ou_estimator.rs`
|
||||
|
||||
**Python API**:
|
||||
```python
|
||||
from optimizr import estimate_ou_params_py
|
||||
import numpy as np
|
||||
|
||||
spread = np.random.randn(10_000)
|
||||
kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=1/252)
|
||||
# Simulate OU process (for testing)
|
||||
dt = 1/252 # Daily data
|
||||
T = 1000
|
||||
kappa_true, theta_true, sigma_true = 3.0, 0.0, 0.2
|
||||
spread = [0.0]
|
||||
for _ in range(T-1):
|
||||
dx = kappa_true * (theta_true - spread[-1]) * dt + \
|
||||
sigma_true * np.sqrt(dt) * np.random.randn()
|
||||
spread.append(spread[-1] + dx)
|
||||
|
||||
spread = np.array(spread)
|
||||
|
||||
# Estimate parameters
|
||||
kappa, theta, sigma, half_life = estimate_ou_params_py(spread, dt=dt)
|
||||
|
||||
print(f"True: κ={kappa_true:.2f}, θ={theta_true:.3f}, σ={sigma_true:.3f}")
|
||||
print(f"Estimated: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
|
||||
print(f"Half-life: {half_life:.1f} periods ({half_life*252:.1f} days)")
|
||||
```
|
||||
|
||||
Method-of-moments / MLE fit returns $(\kappa, \theta, \sigma, \text{half-life})$. Use a few thousand samples for stability; winsorize heavy tails if needed.
|
||||
**Method**: Maximum likelihood estimation (MLE) using analytical formulas for discrete-time OU process.
|
||||
|
||||
## Kalman filtering (linear, EKF, UKF)
|
||||
**Parameters**:
|
||||
- `spread`: Time series of spread values (1D numpy array)
|
||||
- `dt`: Time step in years (e.g., 1/252 for daily data, 1/52 for weekly)
|
||||
|
||||
**Returns**:
|
||||
- `kappa`: Mean reversion speed (annualized)
|
||||
- `theta`: Long-run mean
|
||||
- `sigma`: Volatility (annualized)
|
||||
- `half_life`: Half-life in time step units ($\ln(2)/\kappa \cdot dt^{-1}$)
|
||||
|
||||
**Practical tips**:
|
||||
- Use at least 500-1000 observations for stable estimates
|
||||
- Check half-life: typical pairs have half-life 5-60 days
|
||||
- Winsorize extreme outliers (e.g., clip at ±5σ) if needed
|
||||
- For rolling estimates, use expanding or rolling windows of 250-500 periods
|
||||
|
||||
### 7. Kalman Filtering
|
||||
|
||||
State-space filtering for latent variable estimation and forecasting.
|
||||
|
||||
**Implementation**: `src/optimal_control/kalman_filter.rs`, `src/optimal_control/kalman_py_bindings.rs`
|
||||
|
||||
#### Linear Kalman Filter
|
||||
|
||||
For linear Gaussian state-space models:
|
||||
$$
|
||||
\begin{aligned}
|
||||
x_{t+1} &= F x_t + B u_t + w_t, \quad w_t \sim \mathcal{N}(0, Q) \\
|
||||
y_t &= H x_t + v_t, \quad v_t \sim \mathcal{N}(0, R)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
**Python API**:
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import LinearKalmanFilter
|
||||
import numpy as np
|
||||
|
||||
F = [[1.0, 1.0], [0.0, 1.0]]
|
||||
H = [[1.0, 0.0]]
|
||||
Q = [[1e-4, 0.0], [0.0, 1e-4]]
|
||||
R = [[1e-2]]
|
||||
# Define system matrices
|
||||
F = [[1.0, 1.0], [0.0, 1.0]] # State transition (2×2)
|
||||
H = [[1.0, 0.0]] # Observation matrix (1×2)
|
||||
Q = [[1e-4, 0.0], [0.0, 1e-4]] # Process noise covariance
|
||||
R = [[1e-2]] # Measurement noise covariance
|
||||
|
||||
# Initialize filter
|
||||
kf = LinearKalmanFilter(
|
||||
f_matrix=F,
|
||||
h_matrix=H,
|
||||
@@ -80,15 +337,274 @@ kf = LinearKalmanFilter(
|
||||
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
|
||||
)
|
||||
|
||||
kf.predict(control=[0.0, 0.0])
|
||||
kf.update(observation=[1.2])
|
||||
state = kf.get_state()
|
||||
# Online filtering loop
|
||||
observations = np.random.randn(100)
|
||||
states = []
|
||||
for obs in observations:
|
||||
kf.predict(control=[0.0, 0.0]) # Prediction step
|
||||
kf.update(observation=[obs]) # Correction step
|
||||
state = kf.get_state()
|
||||
states.append(state)
|
||||
|
||||
states = np.array(states)
|
||||
print(f"Final state estimate: {states[-1]}")
|
||||
```
|
||||
|
||||
- Interfaces: `LinearKalmanFilter`, `UnscentedKalmanFilter`, and `KalmanState` for batch `filter` and smoothing.
|
||||
- Concept: prediction (dynamics prior) + correction (measurement residual); RTS smoother refines past states.
|
||||
**Use cases**:
|
||||
- Tracking latent spread dynamics with noise
|
||||
- State estimation for control (e.g., estimate velocity from noisy position)
|
||||
- Online parameter adaptation
|
||||
|
||||
## Practical notes
|
||||
- Rust backend (`optimizr._core`) must be present for control utilities; install from source if wheels are unavailable.
|
||||
- Grids: for HJB, `n_points≈400` is stable; widen `n_std` for volatile spreads.
|
||||
- Combine with Mean Field Games: see `mean_field_games.md` for population dynamics; use Kalman estimates as control inputs if needed.
|
||||
#### Extended Kalman Filter (EKF)
|
||||
|
||||
For nonlinear systems with local linearization.
|
||||
|
||||
**Use case**: Nonlinear spread dynamics, regime probabilities as states.
|
||||
|
||||
#### Unscented Kalman Filter (UKF)
|
||||
|
||||
For highly nonlinear systems using sigma-point approximation.
|
||||
|
||||
**Python API**:
|
||||
```python
|
||||
from optimizr import UnscentedKalmanFilter
|
||||
|
||||
ukf = UnscentedKalmanFilter(
|
||||
state_dim=2,
|
||||
obs_dim=1,
|
||||
q_matrix=Q,
|
||||
r_matrix=R,
|
||||
initial_state=[0.0, 0.0],
|
||||
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
|
||||
)
|
||||
# Similar predict/update interface
|
||||
```
|
||||
|
||||
**Use case**: Jump diffusion models, volatility estimation, option pricing.
|
||||
|
||||
### 8. Backtesting Framework
|
||||
|
||||
Backtests optimal switching strategies on historical data.
|
||||
|
||||
**Implementation**: `src/optimal_control/backtest.rs`
|
||||
|
||||
**Python API**:
|
||||
```python
|
||||
from optimizr import backtest_optimal_switching_py
|
||||
|
||||
# First, get optimal thresholds
|
||||
lower, upper, _, _ = solve_hjb_py(
|
||||
kappa=3.0, theta=0.0, sigma=0.2,
|
||||
rho=0.04, transaction_cost=0.001
|
||||
)
|
||||
|
||||
# Backtest on historical spread
|
||||
metrics = backtest_optimal_switching_py(
|
||||
spread=spread, # Historical spread data
|
||||
lower_bound=lower, # Optimal buy threshold
|
||||
upper_bound=upper, # Optimal sell threshold
|
||||
transaction_cost=0.001, # Must match HJB solver
|
||||
)
|
||||
|
||||
(
|
||||
total_return, # Cumulative return
|
||||
sharpe, # Annualized Sharpe ratio
|
||||
max_dd, # Maximum drawdown
|
||||
n_trades, # Number of round-trip trades
|
||||
win_rate, # Fraction of profitable trades
|
||||
pnl_path, # P&L time series
|
||||
) = metrics
|
||||
|
||||
print(f"Return: {total_return:.2%}, Sharpe: {sharpe:.2f}")
|
||||
print(f"Max DD: {max_dd:.2%}, Trades: {n_trades}, Win rate: {win_rate:.2%}")
|
||||
|
||||
# Plot P&L path
|
||||
import matplotlib.pyplot as plt
|
||||
plt.figure(figsize=(12, 6))
|
||||
plt.subplot(2, 1, 1)
|
||||
plt.plot(spread, label='Spread')
|
||||
plt.axhline(lower, color='r', linestyle='--', label='Lower')
|
||||
plt.axhline(upper, color='g', linestyle='--', label='Upper')
|
||||
plt.legend()
|
||||
plt.subplot(2, 1, 2)
|
||||
plt.plot(pnl_path, label='P&L')
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
```
|
||||
|
||||
**Metrics interpretation**:
|
||||
- `total_return`: Should be positive with low transaction costs
|
||||
- `sharpe`: Good values > 1.0, excellent > 2.0
|
||||
- `max_dd`: Risk metric, compare to expected return
|
||||
- `n_trades`: Too many → excessive costs; too few → missing opportunities
|
||||
- `win_rate`: Typically 40-60% for mean-reversion strategies
|
||||
|
||||
**Parameter tuning**:
|
||||
- If `win_rate` low but `max_dd` high → bands too narrow, increase `transaction_cost` or `rho`
|
||||
- If `n_trades` low → bands too wide, decrease `transaction_cost` or `rho`
|
||||
- Compare Sharpe ratios across different parameter settings
|
||||
|
||||
## Complete Workflow Example
|
||||
|
||||
Here's a complete optimal control pipeline for pairs trading:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from optimizr import (
|
||||
estimate_ou_params_py,
|
||||
solve_hjb_py,
|
||||
backtest_optimal_switching_py,
|
||||
HMM
|
||||
)
|
||||
|
||||
# 1. Load price data (example with simulated data)
|
||||
np.random.seed(42)
|
||||
T = 5000
|
||||
dt = 1/252
|
||||
|
||||
# Simulate cointegrated pair
|
||||
price_A = 100 * np.exp(np.cumsum(0.0001 + 0.01*np.sqrt(dt)*np.random.randn(T)))
|
||||
price_B = 100 * np.exp(np.cumsum(0.0001 + 0.01*np.sqrt(dt)*np.random.randn(T)))
|
||||
spread = np.log(price_A) - np.log(price_B)
|
||||
|
||||
# Split into train/test
|
||||
train_spread = spread[:3000]
|
||||
test_spread = spread[3000:]
|
||||
|
||||
# 2. Estimate OU parameters
|
||||
kappa, theta, sigma, half_life = estimate_ou_params_py(train_spread, dt=dt)
|
||||
print(f"OU parameters: κ={kappa:.2f}, θ={theta:.3f}, σ={sigma:.3f}")
|
||||
print(f"Half-life: {half_life:.1f} days")
|
||||
|
||||
# 3. Solve HJB for optimal thresholds
|
||||
lower, upper, residual, iters = solve_hjb_py(
|
||||
kappa=kappa,
|
||||
theta=theta,
|
||||
sigma=sigma,
|
||||
rho=0.04,
|
||||
transaction_cost=0.001,
|
||||
n_points=400,
|
||||
max_iter=2000,
|
||||
tolerance=1e-7,
|
||||
)
|
||||
print(f"Optimal thresholds: ({lower:.3f}, {upper:.3f})")
|
||||
print(f"Converged in {iters} iterations, residual={residual:.2e}")
|
||||
|
||||
# 4. Backtest on out-of-sample data
|
||||
metrics = backtest_optimal_switching_py(
|
||||
spread=test_spread,
|
||||
lower_bound=lower,
|
||||
upper_bound=upper,
|
||||
transaction_cost=0.001,
|
||||
)
|
||||
total_return, sharpe, max_dd, n_trades, win_rate, pnl_path = metrics
|
||||
|
||||
print(f"\nBacktest Results:")
|
||||
print(f" Total Return: {total_return:.2%}")
|
||||
print(f" Sharpe Ratio: {sharpe:.2f}")
|
||||
print(f" Max Drawdown: {max_dd:.2%}")
|
||||
print(f" # Trades: {n_trades}")
|
||||
print(f" Win Rate: {win_rate:.2%}")
|
||||
|
||||
# 5. Optional: Regime-aware control with HMM
|
||||
returns = np.diff(train_spread)
|
||||
hmm = HMM(n_states=2)
|
||||
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
|
||||
regimes = hmm.predict(returns.reshape(-1, 1))
|
||||
|
||||
# Estimate OU per regime and get regime-specific thresholds
|
||||
for regime_id in range(2):
|
||||
mask = (regimes == regime_id)
|
||||
spread_regime = train_spread[1:][mask]
|
||||
k, t, s, _ = estimate_ou_params_py(spread_regime, dt=dt)
|
||||
l, u, _, _ = solve_hjb_py(k, t, s, 0.04, 0.001)
|
||||
print(f"Regime {regime_id}: κ={k:.2f}, thresholds=({l:.3f}, {u:.3f})")
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Computational Complexity
|
||||
|
||||
- **HJB Solver**: $O(N \cdot K)$ where $N$ is `n_points`, $K$ is policy iterations (~10-50)
|
||||
- **OU Estimation**: $O(T)$ where $T$ is time series length (closed-form MLE)
|
||||
- **Kalman Filter**: $O(T \cdot d^3)$ where $d$ is state dimension (matrix inversion per step)
|
||||
- **Backtesting**: $O(T)$ single pass through data
|
||||
|
||||
### Typical Runtimes (on modern CPU)
|
||||
|
||||
- HJB solve (400 points): ~10-50ms
|
||||
- OU estimation (5000 samples): ~1ms
|
||||
- Kalman filter (1000 steps, 2D state): ~10ms
|
||||
- Backtest (5000 samples): ~5ms
|
||||
|
||||
### Memory Requirements
|
||||
|
||||
- HJB solver: $O(N)$ for grid storage (~few KB)
|
||||
- Kalman filter: $O(d^2)$ for covariance matrices (~few KB for small $d$)
|
||||
- Backtesting: $O(T)$ for P&L path storage (~few MB for long histories)
|
||||
|
||||
## Integration with Other Modules
|
||||
|
||||
### With HMM (Hidden Markov Models)
|
||||
- Use HMM to detect market regimes
|
||||
- Estimate OU parameters per regime
|
||||
- Apply regime-specific optimal controls
|
||||
- See `api/hmm.md` for HMM documentation
|
||||
|
||||
### With Mean Field Games
|
||||
- Use optimal control as individual agent strategy
|
||||
- Aggregate across population for mean-field dynamics
|
||||
- See `algorithms/mean_field_games.md` for MFG theory
|
||||
|
||||
### With Sparse Optimization
|
||||
- Use Kalman-filtered states as inputs to sparse controllers
|
||||
- Combine L1-regularized control with HJB thresholds
|
||||
- See `algorithms/sparse_optimization.md`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### HJB solver not converging
|
||||
- **Symptom**: `residual > tolerance` after `max_iter`
|
||||
- **Fix**: Increase `max_iter` (try 5000-10000); reduce `tolerance` requirement; check that OU parameters are reasonable
|
||||
|
||||
### Thresholds outside grid bounds
|
||||
- **Symptom**: Optimal thresholds at grid edges
|
||||
- **Fix**: Increase `n_std` (try 6-8); check OU parameter estimates (very high σ needs wider grid)
|
||||
|
||||
### OU estimates unstable
|
||||
- **Symptom**: Negative `kappa` or extreme `half_life`
|
||||
- **Fix**: Use more data (>1000 samples); check for non-stationarity; consider winsorizing outliers
|
||||
|
||||
### Backtest Sharpe ratio low
|
||||
- **Symptom**: Sharpe < 0.5 despite positive thresholds
|
||||
- **Fix**: Check for regime changes (use HMM); verify spread is actually mean-reverting; adjust `transaction_cost` in HJB solver
|
||||
|
||||
### Kalman filter diverging
|
||||
- **Symptom**: State estimates exploding
|
||||
- **Fix**: Check process noise `Q` is not too large; verify observations are scaled properly; use UKF for strong nonlinearity
|
||||
|
||||
## References
|
||||
|
||||
### Optimal Control Theory
|
||||
- **Fleming, W. H., & Soner, H. M.** (2006). *Controlled Markov Processes and Viscosity Solutions*. Springer.
|
||||
- **Øksendal, B.** (2003). *Stochastic Differential Equations: An Introduction with Applications* (6th ed.). Springer.
|
||||
- **Pham, H.** (2009). *Continuous-time Stochastic Control and Optimization with Financial Applications*. Springer.
|
||||
|
||||
### Viscosity Solutions
|
||||
- **Barles, G., & Souganidis, P. E.** (1991). Convergence of approximation schemes for fully nonlinear second order equations. *Asymptotic Analysis*, 4(3), 271-283.
|
||||
- **Crandall, M. G., Ishii, H., & Lions, P.-L.** (1992). User's guide to viscosity solutions of second order partial differential equations. *Bulletin of the American Mathematical Society*, 27(1), 1-67.
|
||||
|
||||
### Kalman Filtering
|
||||
- **Kalman, R. E.** (1960). A new approach to linear filtering and prediction problems. *Journal of Basic Engineering*, 82(1), 35-45.
|
||||
- **Julier, S. J., & Uhlmann, J. K.** (1997). New extension of the Kalman filter to nonlinear systems. *Signal Processing, Sensor Fusion, and Target Recognition VI*, 3068, 182-193.
|
||||
|
||||
### Financial Applications
|
||||
- **Avellaneda, M., & Lee, J.-H.** (2010). Statistical arbitrage in the US equities market. *Quantitative Finance*, 10(7), 761-782.
|
||||
- **Gatev, E., Goetzmann, W. N., & Rouwenhorst, K. G.** (2006). Pairs trading: Performance of a relative-value arbitrage rule. *The Review of Financial Studies*, 19(3), 797-827.
|
||||
|
||||
## See Also
|
||||
|
||||
- [HMM API Reference](../api/hmm.md) - Hidden Markov Models for regime detection
|
||||
- [Mean Field Games](mean_field_games.md) - Population-level optimal control
|
||||
- [Optimal Control API](../api/optimal_control.md) - Complete function signatures and types
|
||||
|
||||
+564
-9
@@ -1,16 +1,571 @@
|
||||
# API: HMM
|
||||
# API Reference: Hidden Markov Model (HMM)
|
||||
|
||||
The `HMM` class provides a complete implementation of Hidden Markov Models with Gaussian emissions for regime detection, time series modeling, and state inference.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from optimizr import HMM
|
||||
import numpy as np
|
||||
|
||||
# Create model with 2 hidden states (e.g., bull/bear market)
|
||||
model = HMM(n_states=2)
|
||||
model.fit(X, n_iterations=100, tolerance=1e-6)
|
||||
states = model.predict(X)
|
||||
logp = model.score(X)
|
||||
|
||||
# Train on returns data
|
||||
returns = np.random.randn(1000, 1) # Should be 2D: (n_samples, n_features)
|
||||
model.fit(returns, n_iterations=100, tolerance=1e-6)
|
||||
|
||||
# Decode most likely state sequence (Viterbi)
|
||||
states = model.predict(returns)
|
||||
|
||||
# Compute log-likelihood (for model comparison)
|
||||
logp = model.score(returns)
|
||||
|
||||
print(f"Log-likelihood: {logp:.2f}")
|
||||
print(f"Decoded states: {states[:10]}")
|
||||
```
|
||||
|
||||
Parameters
|
||||
- `n_states`: number of hidden regimes
|
||||
- `fit(X, n_iterations=100, tolerance=1e-6)`: train with Baum-Welch
|
||||
- `predict(X)`: Viterbi decoding → `np.ndarray`
|
||||
- `score(X)`: log-likelihood
|
||||
## Constructor
|
||||
|
||||
### `HMM(n_states: int)`
|
||||
|
||||
Creates a new Hidden Markov Model with Gaussian emissions.
|
||||
|
||||
**Parameters:**
|
||||
- `n_states` (int): Number of hidden states/regimes. Common choices:
|
||||
- `n_states=2`: Binary regime (e.g., bull/bear, high/low volatility)
|
||||
- `n_states=3`: Three-regime model (e.g., bull/sideways/bear)
|
||||
- `n_states>3`: Fine-grained regime detection (requires more data)
|
||||
|
||||
**Returns:**
|
||||
- `HMM` object with random initialization
|
||||
|
||||
**Initialization:**
|
||||
- Transition matrix $A$: Uniform with slight self-transition bias
|
||||
- Initial state distribution $\pi$: Uniform
|
||||
- Emission parameters (means $\mu_i$, covariances $\Sigma_i$): From K-means clustering
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# Binary regime model
|
||||
hmm_2 = HMM(n_states=2)
|
||||
|
||||
# Three-regime model for more nuanced detection
|
||||
hmm_3 = HMM(n_states=3)
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- `n_states=2`: Most common, sufficient for many applications
|
||||
- Higher `n_states`: When you have strong prior belief in multiple regimes and sufficient data (>1000 samples per state)
|
||||
|
||||
## Methods
|
||||
|
||||
### `fit(X, n_iterations=100, tolerance=1e-6, n_init=1, random_state=None)`
|
||||
|
||||
Trains the HMM on observed data using the Baum-Welch (Expectation-Maximization) algorithm.
|
||||
|
||||
**Parameters:**
|
||||
- `X` (np.ndarray): Training data of shape `(n_samples, n_features)`
|
||||
- For univariate time series: reshape to `(n, 1)` with `X.reshape(-1, 1)`
|
||||
- For multivariate: pass directly as `(n, d)` where `d` is feature dimension
|
||||
- `n_iterations` (int, default=100): Maximum number of EM iterations
|
||||
- Typical range: 50-200
|
||||
- More iterations → better convergence but slower training
|
||||
- `tolerance` (float, default=1e-6): Convergence threshold
|
||||
- Algorithm stops when log-likelihood improvement < `tolerance`
|
||||
- Typical range: 1e-8 to 1e-4
|
||||
- Smaller values → tighter convergence but more iterations
|
||||
- `n_init` (int, default=1): Number of random initializations
|
||||
- The best model (highest log-likelihood) is kept
|
||||
- Recommended: 5-10 for production models (helps avoid local minima)
|
||||
- `random_state` (int, optional): Random seed for reproducibility
|
||||
|
||||
**Returns:**
|
||||
- `self`: The fitted HMM object (for method chaining)
|
||||
|
||||
**Algorithm: Baum-Welch (EM for HMMs)**
|
||||
|
||||
The Baum-Welch algorithm iteratively refines model parameters:
|
||||
|
||||
1. **E-step**: Compute state occupation probabilities
|
||||
- Forward pass: $\alpha_t(i) = P(O_1, \ldots, O_t, S_t = i \mid \lambda)$
|
||||
- Backward pass: $\beta_t(i) = P(O_{t+1}, \ldots, O_T \mid S_t = i, \lambda)$
|
||||
- State probabilities: $\gamma_t(i) = \frac{\alpha_t(i)\beta_t(i)}{\sum_j \alpha_t(j)\beta_t(j)}$
|
||||
- Transition probabilities: $\xi_t(i,j) = \frac{\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}{\sum_{i,j}\alpha_t(i)a_{ij}b_j(O_{t+1})\beta_{t+1}(j)}$
|
||||
|
||||
2. **M-step**: Update model parameters
|
||||
- Initial probabilities: $\pi_i = \gamma_1(i)$
|
||||
- Transition matrix: $a_{ij} = \frac{\sum_{t=1}^{T-1}\xi_t(i,j)}{\sum_{t=1}^{T-1}\gamma_t(i)}$
|
||||
- Emission means: $\mu_i = \frac{\sum_{t=1}^T \gamma_t(i) O_t}{\sum_{t=1}^T \gamma_t(i)}$
|
||||
- Emission covariances: $\Sigma_i = \frac{\sum_{t=1}^T \gamma_t(i)(O_t - \mu_i)(O_t - \mu_i)^T}{\sum_{t=1}^T \gamma_t(i)}$
|
||||
|
||||
3. **Convergence**: Repeat until log-likelihood change < tolerance
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import HMM
|
||||
|
||||
# Simulate two-regime data
|
||||
np.random.seed(42)
|
||||
n = 2000
|
||||
|
||||
# Regime 1: low volatility (first 1000 samples)
|
||||
regime1 = np.random.normal(0.0, 0.5, 1000)
|
||||
# Regime 2: high volatility (last 1000 samples)
|
||||
regime2 = np.random.normal(0.0, 2.0, 1000)
|
||||
data = np.concatenate([regime1, regime2]).reshape(-1, 1)
|
||||
|
||||
# Train HMM
|
||||
hmm = HMM(n_states=2)
|
||||
hmm.fit(data, n_iterations=200, tolerance=1e-6, n_init=5)
|
||||
|
||||
print("Training complete")
|
||||
```
|
||||
|
||||
**Convergence diagnostics:**
|
||||
```python
|
||||
# Plot log-likelihood over iterations (requires storing history)
|
||||
# Check if converged before max_iter
|
||||
# Verify parameters make sense (e.g., distinct means for each state)
|
||||
```
|
||||
|
||||
**Typical training time:**
|
||||
- 1000 samples, 2 states, 100 iterations: ~50-100ms
|
||||
- 10000 samples, 3 states, 200 iterations: ~500ms-1s
|
||||
|
||||
### `predict(X)`
|
||||
|
||||
Decodes the most likely sequence of hidden states using the Viterbi algorithm.
|
||||
|
||||
**Parameters:**
|
||||
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
|
||||
- Must match feature dimension used in `fit()`
|
||||
|
||||
**Returns:**
|
||||
- `states` (np.ndarray): Most likely state sequence of shape `(n_samples,)`
|
||||
- Values are integers in range `[0, n_states-1]`
|
||||
|
||||
**Algorithm: Viterbi**
|
||||
|
||||
The Viterbi algorithm finds the globally optimal state sequence:
|
||||
|
||||
1. **Initialization**: $\delta_1(i) = \pi_i \cdot b_i(O_1)$
|
||||
2. **Recursion**: $\delta_t(j) = \max_i[\delta_{t-1}(i) \cdot a_{ij}] \cdot b_j(O_t)$
|
||||
3. **Termination**: $P^* = \max_i[\delta_T(i)]$
|
||||
4. **Backtracking**: Trace back from $\arg\max_i[\delta_T(i)]$ to recover state sequence
|
||||
|
||||
**Complexity:** $O(T \cdot K^2)$ where $T$ is sequence length, $K$ is number of states
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# After training (see fit() example)
|
||||
states = hmm.predict(data)
|
||||
|
||||
# Analyze regime distribution
|
||||
unique, counts = np.unique(states, return_counts=True)
|
||||
for state, count in zip(unique, counts):
|
||||
print(f"State {state}: {count} samples ({count/len(states)*100:.1f}%)")
|
||||
|
||||
# Identify regime switches
|
||||
switches = np.where(np.diff(states) != 0)[0]
|
||||
print(f"Number of regime switches: {len(switches)}")
|
||||
|
||||
# Use for trading: buy in regime 0, sell in regime 1
|
||||
current_state = states[-1]
|
||||
if current_state == 0:
|
||||
print("Signal: BUY (low volatility regime)")
|
||||
else:
|
||||
print("Signal: SELL (high volatility regime)")
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- **Regime detection**: Identify market states (bull/bear, high/low vol)
|
||||
- **Trading signals**: Generate buy/sell signals based on regime
|
||||
- **Risk management**: Adjust position size based on estimated regime
|
||||
- **Anomaly detection**: Flag unusual regime transitions
|
||||
|
||||
### `score(X)`
|
||||
|
||||
Computes the log-likelihood of the observation sequence under the fitted model.
|
||||
|
||||
**Parameters:**
|
||||
- `X` (np.ndarray): Observation sequence of shape `(n_samples, n_features)`
|
||||
|
||||
**Returns:**
|
||||
- `logp` (float): Log-likelihood $\log P(O \mid \lambda)$
|
||||
|
||||
**Algorithm: Forward Algorithm**
|
||||
|
||||
The forward algorithm efficiently computes the likelihood:
|
||||
|
||||
1. **Initialization**: $\alpha_1(i) = \pi_i \cdot b_i(O_1)$
|
||||
2. **Induction**: $\alpha_t(j) = \left[\sum_{i=1}^K \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(O_t)$
|
||||
3. **Termination**: $P(O \mid \lambda) = \sum_{i=1}^K \alpha_T(i)$
|
||||
|
||||
**Numerical stability:** Uses log-space computation with scaling to avoid underflow.
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# Model comparison: which number of states fits best?
|
||||
logp_scores = {}
|
||||
for n_states in [2, 3, 4]:
|
||||
hmm = HMM(n_states=n_states)
|
||||
hmm.fit(data, n_iterations=100)
|
||||
logp = hmm.score(data)
|
||||
logp_scores[n_states] = logp
|
||||
print(f"{n_states} states: log-likelihood = {logp:.2f}")
|
||||
|
||||
# Higher log-likelihood is better (but watch for overfitting)
|
||||
best_k = max(logp_scores, key=logp_scores.get)
|
||||
print(f"Best model: {best_k} states")
|
||||
|
||||
# Use BIC for model selection (penalizes complexity)
|
||||
def bic(logp, n_params, n_samples):
|
||||
return -2 * logp + n_params * np.log(n_samples)
|
||||
|
||||
n_samples = len(data)
|
||||
for n_states in [2, 3, 4]:
|
||||
n_params = n_states**2 + 2*n_states # Approx: A, pi, means, variances
|
||||
bic_score = bic(logp_scores[n_states], n_params, n_samples)
|
||||
print(f"{n_states} states: BIC = {bic_score:.2f}")
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- **Model selection**: Compare models with different `n_states` using BIC/AIC
|
||||
- **Convergence monitoring**: Track log-likelihood during training
|
||||
- **Outlier detection**: Low likelihood → data doesn't match model
|
||||
- **Model reliability**: Higher likelihood → better fit (but watch overfitting)
|
||||
|
||||
## Complete Example: Market Regime Detection
|
||||
|
||||
Here's a complete workflow for detecting market regimes in financial data:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from optimizr import HMM
|
||||
|
||||
# 1. Load financial data (example: S&P 500 returns)
|
||||
# In practice, load from your data source
|
||||
np.random.seed(42)
|
||||
n_samples = 2000
|
||||
|
||||
# Simulate returns with regime changes
|
||||
returns = []
|
||||
for i in range(n_samples):
|
||||
if i < 500: # Bull market
|
||||
returns.append(np.random.normal(0.001, 0.01))
|
||||
elif i < 1000: # Correction
|
||||
returns.append(np.random.normal(-0.002, 0.02))
|
||||
elif i < 1500: # Recovery
|
||||
returns.append(np.random.normal(0.001, 0.015))
|
||||
else: # Bear market
|
||||
returns.append(np.random.normal(-0.001, 0.025))
|
||||
|
||||
returns = np.array(returns).reshape(-1, 1)
|
||||
|
||||
# 2. Train HMM with multiple initializations
|
||||
print("Training HMM...")
|
||||
hmm = HMM(n_states=3) # 3 regimes: bull, neutral, bear
|
||||
hmm.fit(returns, n_iterations=200, tolerance=1e-6, n_init=10)
|
||||
|
||||
# 3. Decode regimes
|
||||
states = hmm.predict(returns)
|
||||
|
||||
# 4. Analyze regimes
|
||||
print("\nRegime Statistics:")
|
||||
for state_id in range(3):
|
||||
mask = (states == state_id)
|
||||
state_returns = returns[mask]
|
||||
mean_ret = np.mean(state_returns)
|
||||
std_ret = np.std(state_returns)
|
||||
count = np.sum(mask)
|
||||
print(f"State {state_id}:")
|
||||
print(f" Count: {count} ({count/len(returns)*100:.1f}%)")
|
||||
print(f" Mean return: {mean_ret:.4f}")
|
||||
print(f" Volatility: {std_ret:.4f}")
|
||||
print(f" Sharpe (annualized): {mean_ret/std_ret * np.sqrt(252):.2f}")
|
||||
|
||||
# 5. Identify regime switches
|
||||
switches = np.where(np.diff(states) != 0)[0] + 1
|
||||
print(f"\nRegime switches: {len(switches)}")
|
||||
print(f"Average regime duration: {len(returns)/len(switches):.1f} days")
|
||||
|
||||
# 6. Visualize regimes
|
||||
plt.figure(figsize=(14, 8))
|
||||
|
||||
# Plot returns with regime colors
|
||||
plt.subplot(3, 1, 1)
|
||||
colors = ['green', 'yellow', 'red']
|
||||
for state_id in range(3):
|
||||
mask = (states == state_id)
|
||||
plt.scatter(np.where(mask)[0], returns[mask],
|
||||
c=colors[state_id], alpha=0.5, s=10,
|
||||
label=f'State {state_id}')
|
||||
plt.ylabel('Returns')
|
||||
plt.title('Returns colored by HMM regime')
|
||||
plt.legend()
|
||||
plt.grid(True, alpha=0.3)
|
||||
|
||||
# Plot cumulative returns per regime
|
||||
plt.subplot(3, 1, 2)
|
||||
cumulative = np.cumsum(returns.flatten())
|
||||
plt.plot(cumulative, color='black', linewidth=1)
|
||||
for switch in switches:
|
||||
plt.axvline(switch, color='red', alpha=0.3, linestyle='--')
|
||||
plt.ylabel('Cumulative Returns')
|
||||
plt.title('Cumulative returns with regime switches')
|
||||
plt.grid(True, alpha=0.3)
|
||||
|
||||
# Plot state sequence
|
||||
plt.subplot(3, 1, 3)
|
||||
plt.plot(states, linewidth=0.5)
|
||||
plt.ylabel('State')
|
||||
plt.xlabel('Time')
|
||||
plt.title('Decoded state sequence')
|
||||
plt.yticks(range(3))
|
||||
plt.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('hmm_regime_detection.png', dpi=150)
|
||||
print("\nPlot saved to hmm_regime_detection.png")
|
||||
|
||||
# 7. Generate trading signals
|
||||
current_state = states[-1]
|
||||
state_returns = returns[states == current_state]
|
||||
expected_return = np.mean(state_returns)
|
||||
expected_vol = np.std(state_returns)
|
||||
|
||||
print(f"\nCurrent regime: State {current_state}")
|
||||
print(f"Expected return: {expected_return:.4f}")
|
||||
print(f"Expected volatility: {expected_vol:.4f}")
|
||||
|
||||
if expected_return > 0.0005:
|
||||
signal = "BUY"
|
||||
position_size = 1.0
|
||||
elif expected_return < -0.0005:
|
||||
signal = "SELL"
|
||||
position_size = 0.0
|
||||
else:
|
||||
signal = "HOLD"
|
||||
position_size = 0.5
|
||||
|
||||
print(f"Trading signal: {signal}")
|
||||
print(f"Recommended position size: {position_size*100:.0f}%")
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Model Selection with BIC
|
||||
|
||||
Choose the optimal number of states using Bayesian Information Criterion:
|
||||
|
||||
```python
|
||||
from optimizr import HMM
|
||||
import numpy as np
|
||||
|
||||
def bic_score(hmm, X):
|
||||
"""Compute BIC for HMM: BIC = -2*log(L) + k*log(n)"""
|
||||
logp = hmm.score(X)
|
||||
n_states = hmm.n_states # Assuming this attribute exists
|
||||
n_features = X.shape[1]
|
||||
# Parameters: transition matrix + initial prob + means + covariances
|
||||
k = n_states**2 + n_states + n_states*n_features + n_states*n_features**2
|
||||
n = X.shape[0]
|
||||
return -2*logp + k*np.log(n)
|
||||
|
||||
# Test different numbers of states
|
||||
results = []
|
||||
for n_states in range(2, 6):
|
||||
hmm = HMM(n_states=n_states)
|
||||
hmm.fit(data, n_iterations=100, n_init=5)
|
||||
bic = bic_score(hmm, data)
|
||||
logp = hmm.score(data)
|
||||
results.append((n_states, logp, bic))
|
||||
print(f"{n_states} states: log-likelihood={logp:.2f}, BIC={bic:.2f}")
|
||||
|
||||
# Best model has lowest BIC
|
||||
best_n_states = min(results, key=lambda x: x[2])[0]
|
||||
print(f"\nBest model: {best_n_states} states")
|
||||
```
|
||||
|
||||
### Integration with Optimal Control
|
||||
|
||||
Combine HMM regime detection with regime-specific optimal control:
|
||||
|
||||
```python
|
||||
from optimizr import HMM, estimate_ou_params_py, solve_hjb_py
|
||||
|
||||
# 1. Detect regimes with HMM
|
||||
returns = np.diff(spread)
|
||||
hmm = HMM(n_states=2)
|
||||
hmm.fit(returns.reshape(-1, 1), n_iterations=100)
|
||||
regimes = hmm.predict(returns.reshape(-1, 1))
|
||||
|
||||
# 2. Estimate OU parameters per regime
|
||||
thresholds = {}
|
||||
for regime_id in range(2):
|
||||
mask = (regimes == regime_id)
|
||||
spread_regime = spread[1:][mask] # Align with returns
|
||||
|
||||
# Estimate OU parameters
|
||||
kappa, theta, sigma, half_life = estimate_ou_params_py(
|
||||
spread_regime, dt=1/252
|
||||
)
|
||||
|
||||
# Solve HJB for regime-specific thresholds
|
||||
lower, upper, _, _ = solve_hjb_py(
|
||||
kappa=kappa, theta=theta, sigma=sigma,
|
||||
rho=0.04, transaction_cost=0.001
|
||||
)
|
||||
|
||||
thresholds[regime_id] = (lower, upper)
|
||||
print(f"Regime {regime_id}: κ={kappa:.2f}, thresholds=({lower:.3f}, {upper:.3f})")
|
||||
|
||||
# 3. Apply regime-aware trading
|
||||
current_regime = regimes[-1]
|
||||
lower, upper = thresholds[current_regime]
|
||||
current_spread = spread[-1]
|
||||
|
||||
if current_spread < lower:
|
||||
action = "BUY"
|
||||
elif current_spread > upper:
|
||||
action = "SELL"
|
||||
else:
|
||||
action = "HOLD"
|
||||
|
||||
print(f"\nCurrent regime: {current_regime}")
|
||||
print(f"Current spread: {current_spread:.3f}")
|
||||
print(f"Thresholds: ({lower:.3f}, {upper:.3f})")
|
||||
print(f"Action: {action}")
|
||||
```
|
||||
|
||||
### Multivariate HMM
|
||||
|
||||
For multiple features (e.g., returns + volume + volatility):
|
||||
|
||||
```python
|
||||
# Prepare multivariate data
|
||||
returns = np.random.randn(1000, 1)
|
||||
volume = np.random.randn(1000, 1)
|
||||
volatility = np.random.randn(1000, 1)
|
||||
|
||||
# Stack features
|
||||
X = np.hstack([returns, volume, volatility]) # Shape: (1000, 3)
|
||||
|
||||
# Train multivariate HMM
|
||||
hmm = HMM(n_states=3)
|
||||
hmm.fit(X, n_iterations=150)
|
||||
|
||||
# Decode regimes based on all features
|
||||
states = hmm.predict(X)
|
||||
|
||||
# Each state now captures joint patterns in returns, volume, and volatility
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Data Preparation
|
||||
|
||||
1. **Scaling**: Standardize features to similar scales
|
||||
```python
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
scaler = StandardScaler()
|
||||
X_scaled = scaler.fit_transform(X)
|
||||
```
|
||||
|
||||
2. **Stationarity**: Ensure time series is stationary (use returns, not prices)
|
||||
```python
|
||||
returns = np.diff(np.log(prices)) # Log returns
|
||||
```
|
||||
|
||||
3. **Outlier handling**: Winsorize extreme values
|
||||
```python
|
||||
from scipy.stats import mstats
|
||||
X_winsorized = mstats.winsorize(X, limits=[0.01, 0.01])
|
||||
```
|
||||
|
||||
### Model Training
|
||||
|
||||
1. **Multiple initializations**: Use `n_init=5-10` to avoid local minima
|
||||
2. **Convergence**: Monitor log-likelihood, ensure convergence before `max_iter`
|
||||
3. **Validation**: Use held-out data to verify generalization
|
||||
|
||||
### Parameter Selection
|
||||
|
||||
1. **Number of states**: Start with 2-3, increase if necessary
|
||||
2. **Iterations**: 100-200 typically sufficient
|
||||
3. **Tolerance**: 1e-6 for production, 1e-4 for quick experimentation
|
||||
|
||||
### Practical Tips
|
||||
|
||||
1. **Minimum data**: Use at least 500 samples per state (1000+ for 2-state model)
|
||||
2. **Regime persistence**: Check average regime duration is meaningful (not too short)
|
||||
3. **Physical interpretation**: Verify decoded regimes make sense (e.g., high-vol state has higher variance)
|
||||
4. **Robustness**: Test on multiple time periods, verify stability
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Model not converging
|
||||
- **Symptom**: Log-likelihood oscillating or not improving
|
||||
- **Fix**: Increase `n_iterations`; try different `n_init`; check data scaling
|
||||
|
||||
### All samples assigned to one state
|
||||
- **Symptom**: `predict()` returns all 0s or all 1s
|
||||
- **Fix**: Reduce `n_states`; check data has sufficient variation; verify stationarity
|
||||
|
||||
### Unrealistic regime switches
|
||||
- **Symptom**: State changes every few samples
|
||||
- **Fix**: Add transition probability constraints (requires model extension); increase minimum regime duration
|
||||
|
||||
### Poor out-of-sample performance
|
||||
- **Symptom**: High in-sample log-likelihood but poor predictions on new data
|
||||
- **Fix**: Reduce `n_states` (overfitting); use cross-validation; add regularization
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Computational Complexity
|
||||
|
||||
- **Training (Baum-Welch)**: $O(I \cdot T \cdot K^2)$
|
||||
- $I$: number of iterations (~100-200)
|
||||
- $T$: sequence length
|
||||
- $K$: number of states
|
||||
|
||||
- **Prediction (Viterbi)**: $O(T \cdot K^2)$
|
||||
|
||||
- **Scoring (Forward)**: $O(T \cdot K^2)$
|
||||
|
||||
### Memory Requirements
|
||||
|
||||
- Model parameters: $O(K^2 + K \cdot d^2)$ where $d$ is feature dimension
|
||||
- Forward/backward matrices: $O(T \cdot K)$
|
||||
|
||||
### Typical Runtimes (on modern CPU)
|
||||
|
||||
- Train (1000 samples, 2 states, 100 iter): ~50-100ms
|
||||
- Train (10000 samples, 3 states, 200 iter): ~500ms-1s
|
||||
- Predict (1000 samples, 2 states): ~5-10ms
|
||||
- Score (1000 samples, 2 states): ~5-10ms
|
||||
|
||||
## References
|
||||
|
||||
### Hidden Markov Models
|
||||
- **Rabiner, L. R.** (1989). A tutorial on hidden Markov models and selected applications in speech recognition. *Proceedings of the IEEE*, 77(2), 257-286.
|
||||
- **Murphy, K. P.** (2012). *Machine Learning: A Probabilistic Perspective*. MIT Press. (Chapter 17: Markov and hidden Markov models)
|
||||
|
||||
### Financial Applications
|
||||
- **Guidolin, M., & Timmermann, A.** (2008). International asset allocation under regime switching, skew, and kurtosis preferences. *The Review of Financial Studies*, 21(2), 889-935.
|
||||
- **Nystrup, P., Madsen, H., & Lindström, E.** (2015). Stylised facts of financial time series and hidden Markov models in continuous time. *Quantitative Finance*, 15(9), 1531-1541.
|
||||
- **Ang, A., & Bekaert, G.** (2002). Regime switches in interest rates. *Journal of Business & Economic Statistics*, 20(2), 163-182.
|
||||
|
||||
### Algorithms
|
||||
- **Forney, G. D.** (1973). The Viterbi algorithm. *Proceedings of the IEEE*, 61(3), 268-278.
|
||||
- **Baum, L. E., Petrie, T., Soules, G., & Weiss, N.** (1970). A maximization technique occurring in the statistical analysis of probabilistic functions of Markov chains. *The Annals of Mathematical Statistics*, 41(1), 164-171.
|
||||
|
||||
## See Also
|
||||
|
||||
- [HMM Algorithms](../algorithms/hmm.md) - Detailed mathematical foundations (Forward-Backward, Viterbi, Baum-Welch)
|
||||
- [Optimal Control](../algorithms/optimal_control.md) - Integrate HMM regimes with optimal control
|
||||
- [Optimal Control API](optimal_control.md) - API reference for control algorithms
|
||||
|
||||
Reference in New Issue
Block a user