2026-02-09 16:15:41 +01:00
# API: Optimal Control / Kalman
2026-02-09 17:09:27 +01:00
High-level bindings exposed by the `optimizr` Python package. All functions require the Rust extension (`optimizr._core` ).
2026-02-09 17:27:35 +01:00
**When to use this module**
- Threshold trading / switching problems solved via HJB (with and without frictions)
- State estimation and smoothing (Kalman, EKF, UKF)
- Parameter inference for mean-reverting spreads (OU) feeding into control logic
2026-02-09 17:09:27 +01:00
## Hamilton– Jacobi– Bellman (HJB) solvers
```python
from optimizr import solve_hjb_py , solve_hjb_full_py
# Switching boundaries for a mean-reverting spread (OU process)
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 ,
)
# Full state (grid + derivatives) for research/visualization
( lower , upper , residual , iters , x_grid , value , grad , hess ) = solve_hjb_full_py (
kappa = 3.0 , theta = 0.0 , sigma = 0.2 , rho = 0.04 ,
transaction_cost = 0.001 , n_points = 400 ,
)
```
### Model
We assume an Ornstein– Uhlenbeck process $dX_t = \kappa(\theta - X_t)\,dt + \sigma\,dW_t$ with quadratic transaction costs. The HJB on grid $x \in [-n_{std}\,\sigma/\sqrt{\kappa},\; n_{std}\,\sigma/\sqrt{\kappa}]$ solves
$$
\rho V(x) = \min\Big\{ \tfrac12 \sigma^2 V_{xx}(x) + \kappa(\theta - x) V_x(x),\; V(x) + c_{\text{buy}},\; V(x) + c_{\text{sell}} \Big\}.
$$
`solve_hjb_py` returns optimal buy/sell thresholds; `solve_hjb_full_py` also returns $V$, $V_x$, and $V_{xx}$ for diagnostics.
2026-02-09 17:27:35 +01:00
**Diagnostic tips:**
- Plot $V_x$ to verify smoothness near the boundaries; kinks often signal insufficient grid resolution.
- Track `residual` and `iterations` to spot non-convergence; loosen `tolerance` or increase `max_iter` if needed.
2026-02-09 17:09:27 +01:00
## OU parameter estimation
2026-02-09 16:15:41 +01:00
```python
2026-02-09 17:09:27 +01:00
import numpy as np
from optimizr import estimate_ou_params_py
spread = np . random . randn ( 10_000 )
kappa , theta , sigma , half_life = estimate_ou_params_py ( spread , dt = 1 / 252 )
```
Method-of-moments / MLE estimation for
$$
X_{t+1} = X_t e^{-\kappa \Delta t} + \theta(1-e^{-\kappa \Delta t}) + \eta_t, \quad \eta_t \sim \mathcal{N}\Big(0,\; \tfrac{\sigma^2}{2\kappa}(1-e^{-2\kappa \Delta t})\Big).
$$
Returns $(\kappa, \theta, \sigma, \text{half\_life})$.
2026-02-09 17:27:35 +01:00
**Practical guidance:** Use at least a few thousand samples for stable estimates; heavy-tailed series benefit from pre-whitening or winsorizing before fitting.
2026-02-09 17:09:27 +01:00
## 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
```
Applies HJB thresholds to historical spreads and reports return, Sharpe ratio, drawdown, trade count, win rate, and PnL path.
2026-02-09 17:27:35 +01:00
**What to inspect:**
- `win_rate` alongside `max_drawdown` to balance aggressiveness
- PnL path for regime shifts; combine with HMM states if you need regime-aware controls
2026-02-09 17:09:27 +01:00
## Kalman filtering (linear, EKF, UKF)
```python
import numpy as np
from optimizr import LinearKalmanFilter , KalmanState
F = [[ 1.0 , 1.0 ], [ 0.0 , 1.0 ]] # constant-velocity model
H = [[ 1.0 , 0.0 ]] # observe position only
Q = [[ 1e-4 , 0.0 ], [ 0.0 , 1e-4 ]]
R = [[ 1e-2 ]]
kf = LinearKalmanFilter (
f_matrix = F ,
h_matrix = H ,
q_matrix = Q ,
r_matrix = R ,
initial_state = [ 0.0 , 0.0 ],
initial_covariance = [[ 1.0 , 0.0 ], [ 0.0 , 1.0 ]],
)
2026-02-09 16:15:41 +01:00
2026-02-09 17:09:27 +01:00
kf . predict ( control = [ 0.0 , 0.0 ]) # optional control input via B matrix
kf . update ( observation = [ 1.2 ])
state = kf . get_state () # KalmanState with getters for mean/cov
2026-02-09 16:15:41 +01:00
2026-02-09 17:09:27 +01:00
# Batch filtering
result = kf . filter ( observations = [[ 1.0 ], [ 1.4 ], [ 1.9 ]], controls = None )
states = result . get_states ()
log_likelihoods = result . get_log_likelihoods ()
2026-02-09 16:15:41 +01:00
```
2026-02-09 17:09:27 +01:00
### Notes
- `LinearKalmanFilter` implements `predict` , `update` , and batch `filter` .
- `KalmanState` exposes `get_state()` , `get_covariance()` , and `get_log_likelihood()` .
- Extended/Unscented Kalman filters share the same interface (see `UnscentedKalmanFilter` in the Rust module) and are exported through the same bindings.
- For smoothing, use the Rauch– Tung– Striebel smoother (`RTSSmoother` ) available in the bindings.
2026-02-09 16:15:41 +01:00
2026-02-09 17:27:35 +01:00
**Conceptual picture:** Kalman filtering = prediction (dynamics prior) + correction (measurement residual). EKF linearizes $f, h$; UKF propagates sigma points for better nonlinear fidelity. RTS smoothing runs backward in time to refine all past states.
2026-02-16 16:42:24 +01:00
See [`examples/notebooks/03_optimal_control_tutorial.ipynb` ](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/03_optimal_control_tutorial.ipynb ) for end-to-end usage combining HJB thresholds, OU estimation, and filtering.