Files
optimiz-rs/src/optimal_control/py_bindings.rs
T
Melvin Avarez 79f51e4775 Release v0.2.0: Comprehensive DE, Mathematical Toolkit, Optimal Control
Major Features:
• Comprehensive Differential Evolution with 5 strategies (rand1, best1, currenttobest1, rand2, best2)
• Adaptive jDE algorithm for self-tuning F and CR parameters
• Convergence tracking with history records and early stopping
• Mathematical toolkit module (780 lines): gradient, hessian, jacobian, statistics, linear algebra
• Optimal control framework: HJB solvers, regime switching, jump diffusion, MRSJD
• Sparse optimization: Sparse PCA, Box-Tao decomposition, ADMM, Elastic Net
• Rayon parallelization infrastructure (ready for pure Rust objectives)

Performance:
• 74-88× speedup for DE vs SciPy
• 50-100× speedup overall vs pure Python

Refactoring & Cleanup:
• Removed 5 legacy files (de_refactored.rs, hmm_legacy.rs, hmm_refactored.rs, mcmc_legacy.rs, mcmc_refactored.rs)
• Modular architecture with trait-based design
• Generic implementations (no domain-specific code)
• Updated Python bindings for new DE API
• Fixed ALL compilation warnings (0 errors, 0 warnings)

Documentation:
• Updated README with v0.2.0 features and benchmarks
• Created RELEASE_NOTES_v0.2.0.md (comprehensive changelog)
• New optimal control tutorial notebook (03_optimal_control_tutorial.ipynb)
• Updated API examples in README
• Created test_release.py for release validation

Version Bumps:
• Cargo.toml: 0.1.0 → 0.2.0
• pyproject.toml: 0.1.0 → 0.2.0
• python/__init__.py: 0.1.0 → 0.2.0

Breaking Changes:
• DE API: mutation_factor/crossover_rate → f/cr
• DE API: use_adaptive_jde → adaptive
• DE API: strategy names simplified (e.g., 'rand/1/bin' → 'rand1')
• DE returns: (x, fun) tuple instead of dict-like object

Known Items (Post-Release):
• Mathematical toolkit functions available in Rust but not yet exposed to Python
• MCMC Python wrapper needs API update to match new Rust implementation
• Tutorial notebooks need DE API updates

Tests: 34 Rust tests passing, core Python functionality validated with test_release.py
2025-12-10 18:54:32 +01:00

242 lines
7.1 KiB
Rust

//! Python bindings for optimal control module
use pyo3::prelude::*;
use numpy::{PyArray1, PyReadonlyArray1};
use crate::optimal_control::*;
/// Solve HJB equation for optimal switching boundaries
#[pyfunction]
#[pyo3(signature = (kappa, theta, sigma, rho=0.04, transaction_cost=0.001, n_points=200, max_iter=2000, tolerance=1e-6, n_std=4.0))]
pub fn solve_hjb_py(
kappa: f64,
theta: f64,
sigma: f64,
rho: f64,
transaction_cost: f64,
n_points: usize,
max_iter: usize,
tolerance: f64,
n_std: f64,
) -> PyResult<(f64, f64, f64, usize)> {
let config = HJBConfig {
kappa,
theta,
sigma,
rho,
transaction_cost,
n_points,
max_iter,
tolerance,
n_std,
};
let solver = HJBSolver::new(config)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e)))?;
let result = solver.solve()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{}", e)))?;
Ok((
result.lower_boundary,
result.upper_boundary,
result.residual,
result.iterations,
))
}
/// Estimate Ornstein-Uhlenbeck parameters
#[pyfunction]
#[pyo3(signature = (spread, dt=1.0/252.0))]
pub fn estimate_ou_params_py(
spread: PyReadonlyArray1<f64>,
dt: f64,
) -> PyResult<(f64, f64, f64, f64)> {
let spread = spread.as_slice()?;
let params = estimate_ou_params(spread, dt)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e)))?;
Ok((params.kappa, params.theta, params.sigma, params.half_life))
}
/// Engle-Granger cointegration test
#[pyfunction]
#[pyo3(signature = (y, x, significance=0.05))]
pub fn engle_granger_test_py(
y: PyReadonlyArray1<f64>,
x: PyReadonlyArray1<f64>,
significance: f64,
) -> PyResult<(f64, f64, f64, bool)> {
let y = y.as_slice()?;
let x = x.as_slice()?;
let result = engle_granger_test(y, x, significance)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e)))?;
Ok((
result.beta,
result.adf_statistic,
result.p_value,
result.is_cointegrated,
))
}
/// Calculate Hurst exponent
#[pyfunction]
#[pyo3(signature = (series, max_lag=20))]
pub fn hurst_exponent_py(
series: PyReadonlyArray1<f64>,
max_lag: usize,
) -> PyResult<f64> {
let series = series.as_slice()?;
hurst_exponent(series, max_lag)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e)))
}
/// Backtest optimal switching strategy
#[pyfunction]
#[pyo3(signature = (spread, lower_bound, upper_bound, transaction_cost=0.001))]
pub fn backtest_optimal_switching_py(
py: Python,
spread: PyReadonlyArray1<f64>,
lower_bound: f64,
upper_bound: f64,
transaction_cost: f64,
) -> PyResult<(f64, f64, f64, usize, f64, Py<PyArray1<f64>>)> {
let spread = spread.as_slice()?;
let result = backtest_optimal_switching(spread, lower_bound, upper_bound, transaction_cost)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{}", e)))?;
let pnl_array = PyArray1::from_vec_bound(py, result.pnl.clone());
Ok((
result.total_return,
result.sharpe_ratio,
result.max_drawdown,
result.num_trades,
result.win_rate,
pnl_array.unbind(),
))
}
/// Test a pair comprehensively (cointegration + OU + HJB + backtest)
#[pyfunction]
#[pyo3(signature = (y, x, significance=0.05, min_hurst=0.45, transaction_cost=0.001))]
pub fn test_pair_py(
py: Python,
y: PyReadonlyArray1<f64>,
x: PyReadonlyArray1<f64>,
significance: f64,
min_hurst: f64,
transaction_cost: f64,
) -> PyResult<Option<PyObject>> {
let y = y.as_slice()?;
let x = x.as_slice()?;
// 1. Cointegration test
let coint_result = match engle_granger_test(y, x, significance) {
Ok(r) => r,
Err(_) => return Ok(None),
};
if !coint_result.is_cointegrated {
return Ok(None);
}
// 2. Hurst exponent
let h = match hurst_exponent(&coint_result.spread, 20) {
Ok(h) => h,
Err(_) => return Ok(None),
};
if h.is_nan() || h > min_hurst {
return Ok(None);
}
// 3. OU parameters
let ou_params = match estimate_ou_params(&coint_result.spread, 1.0 / 252.0) {
Ok(p) => p,
Err(_) => return Ok(None),
};
if ou_params.kappa <= 0.0 || ou_params.kappa.is_nan() {
return Ok(None);
}
// 4. HJB solver
let hjb_config = HJBConfig {
kappa: ou_params.kappa,
theta: ou_params.theta,
sigma: ou_params.sigma,
rho: 0.04,
transaction_cost,
n_points: 200,
max_iter: 2000,
tolerance: 1e-6,
n_std: 4.0,
};
let solver = match HJBSolver::new(hjb_config) {
Ok(s) => s,
Err(_) => return Ok(None),
};
let hjb_result = match solver.solve() {
Ok(r) => r,
Err(_) => return Ok(None),
};
// 5. Backtest
let backtest_result = match backtest_optimal_switching(
&coint_result.spread,
hjb_result.lower_boundary,
hjb_result.upper_boundary,
transaction_cost,
) {
Ok(r) => r,
Err(_) => return Ok(None),
};
// Build result dictionary
let result = pyo3::types::PyDict::new_bound(py);
result.set_item("beta", coint_result.beta)?;
result.set_item("p_value", coint_result.p_value)?;
result.set_item("hurst", h)?;
result.set_item("kappa", ou_params.kappa)?;
result.set_item("theta", ou_params.theta)?;
result.set_item("sigma", ou_params.sigma)?;
result.set_item("half_life", ou_params.half_life)?;
result.set_item("lower_boundary", hjb_result.lower_boundary)?;
result.set_item("upper_boundary", hjb_result.upper_boundary)?;
result.set_item("total_return", backtest_result.total_return)?;
result.set_item("sharpe_ratio", backtest_result.sharpe_ratio)?;
result.set_item("max_drawdown", backtest_result.max_drawdown)?;
result.set_item("num_trades", backtest_result.num_trades)?;
result.set_item("win_rate", backtest_result.win_rate)?;
// Scores
let coint_score = 1.0 - coint_result.p_value;
let meanrev_score = (0.5 - h) / 0.2;
let profit_score = backtest_result.total_return.max(0.0);
let combined_score = coint_score * meanrev_score * (1.0 + profit_score);
result.set_item("coint_score", coint_score)?;
result.set_item("meanrev_score", meanrev_score)?;
result.set_item("profit_score", profit_score)?;
result.set_item("combined_score", combined_score)?;
Ok(Some(result.into()))
}
pub fn register_py_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(solve_hjb_py, m)?)?;
m.add_function(wrap_pyfunction!(estimate_ou_params_py, m)?)?;
m.add_function(wrap_pyfunction!(engle_granger_test_py, m)?)?;
m.add_function(wrap_pyfunction!(hurst_exponent_py, m)?)?;
m.add_function(wrap_pyfunction!(backtest_optimal_switching_py, m)?)?;
m.add_function(wrap_pyfunction!(test_pair_py, m)?)?;
Ok(())
}