Files
optimiz-rs/docs/source/algorithms/optimal_control.md
T
2026-02-09 18:31:14 +01:00

2.7 KiB
Raw Blame History

Optimal Control

HamiltonJacobiBellman (HJB) solvers, regime-switching thresholds, OU parameter estimation, and Kalman filtering utilities backed by Rust.

HJB switching boundaries (OU process)

from optimizr import solve_hjb_py, solve_hjb_full_py

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,
)
print(f"bounds=({lower:.3f}, {upper:.3f}), residual={residual:.2e}, iters={iters}")
  • 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.

Backtesting optimal switching

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

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 fit returns (\kappa, \theta, \sigma, \text{half-life}). Use a few thousand samples for stability; winsorize heavy tails if needed.

Kalman filtering (linear, EKF, UKF)

import numpy as np
from optimizr import LinearKalmanFilter

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]]

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]],
)

kf.predict(control=[0.0, 0.0])
kf.update(observation=[1.2])
state = kf.get_state()
  • Interfaces: LinearKalmanFilter, UnscentedKalmanFilter, and KalmanState for batch filter and smoothing.
  • Concept: prediction (dynamics prior) + correction (measurement residual); RTS smoother refines past states.

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.