Compare commits
52 Commits
v0.3.0
...
release/v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 01bae1f060 | |||
| d780ed81d7 | |||
| e67b0f8376 | |||
| 58c3793b66 | |||
| df6b7f61f6 | |||
| 8a1571c83f | |||
| 49be6c7bf7 | |||
| 5d06f0ab57 | |||
| e80d717aa7 | |||
| d24b8b1036 | |||
| b41618c627 | |||
| 7a964fd9ee | |||
| 48220de0ce | |||
| f6a74716e2 | |||
| 6ab4976e7a | |||
| 6c2ce3d238 | |||
| f7e62cbe6d | |||
| fc8a8d036e | |||
| 5f0c8b0d67 | |||
| 78442d9e69 | |||
| efde8d19a5 | |||
| b10263b4f2 | |||
| bfd0d3c591 | |||
| aa8b1f265d | |||
| fed03f5bdf | |||
| 5f2d2d6187 | |||
| 4714ce03e9 | |||
| 2fa97c2eaa | |||
| e93d43f290 | |||
| 5d4dff2165 | |||
| ec9ae654cb | |||
| 5f44714261 | |||
| 0c0c060bee | |||
| 3e4390e462 | |||
| 6ebd1337fa | |||
| c18788160a | |||
| 815efcb40d | |||
| 6b084ae396 | |||
| 50a4313d5a | |||
| 525015525b | |||
| 07d45297a7 | |||
| a3117fe4a2 | |||
| 0dfb9b803e | |||
| f44280edd7 | |||
| f43659c8e6 | |||
| ea2265cb49 | |||
| 74fbbd369b | |||
| 7a9ab20b89 | |||
| 6e3367c396 | |||
| 50d31d54cc | |||
| 43af949904 | |||
| c1f4c0bde7 |
@@ -22,3 +22,11 @@ Cargo.lock
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
wheels/
|
||||
|
||||
# Build artifacts
|
||||
docs/source/_static/*.aux
|
||||
docs/source/_static/*.log
|
||||
docs/source/_static/*.toc
|
||||
docs/source/theory/*.html
|
||||
docs/source/theory/*.pdf
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
apt_packages:
|
||||
- libopenblas-dev
|
||||
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
- epub
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
@@ -0,0 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to **optimiz-rs** are documented in this file. The format
|
||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
||||
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.1.0] - 2026-05-12
|
||||
|
||||
### Added — purely additive, no existing API changes
|
||||
|
||||
- `optimal_control::matrix_riccati` — RK4 backward solver for the
|
||||
matrix Riccati differential equation
|
||||
`dA/dt = -2 A M A + Q`, plus terminal-condition variants for the
|
||||
associated affine and constant components.
|
||||
- `timeseries_utils::nonsync_covariance` — Hayashi--Yoshida estimator
|
||||
for asynchronous covariance, with parallel matrix variant.
|
||||
- `timeseries_utils::wavelet` — discrete and maximum-overlap wavelet
|
||||
transforms (Haar, Daubechies orders 2--10) with periodic boundaries.
|
||||
- `risk_measures` — empirical and parametric Value-at-Risk and
|
||||
Conditional Value-at-Risk estimators, plus a projected sub-gradient
|
||||
solver for convex CVaR minimisation over the unit simplex.
|
||||
- `graph::laplacian` — combinatorial, symmetric-normalised and
|
||||
random-walk graph Laplacians.
|
||||
- `graph::spectral_clustering` — spectral clustering via Jacobi
|
||||
diagonalisation and Lloyd's algorithm with k-means++ initialisation.
|
||||
- `topology::persistent_homology` — Vietoris--Rips persistent homology
|
||||
by the standard `Z/2` matrix-reduction algorithm.
|
||||
- `topology::bottleneck` — bottleneck distance between persistence
|
||||
diagrams via Hopcroft--Karp matching with binary search.
|
||||
- `volterra::fractional_riccati` — Adams predictor--corrector solver
|
||||
for Caputo fractional ODEs (Diethelm--Ford--Freed 2002).
|
||||
- `volterra::markovian_lift` — multi-exponential approximation of
|
||||
convolution kernels by non-negative least squares on a geometric
|
||||
grid.
|
||||
- `volterra::volterra_solver` — generic second-kind Volterra integral
|
||||
equation solver via product-trapezoidal quadrature.
|
||||
- `volterra::fourier_inversion` — direct trapezoidal Fourier inversion
|
||||
of a characteristic function on a uniform frequency grid.
|
||||
- `signatures::path_signature` — truncated tensor signature of a
|
||||
piecewise-linear path with truncated tensor exponential.
|
||||
- `signatures::log_signature` — truncated tensor logarithm of a
|
||||
signature.
|
||||
- `signatures::random_signature` — Cuchiero--Schmocker--Teichmann
|
||||
random reservoir projection of the signature.
|
||||
- `signatures::signature_kernel` — Salvi--Cass--Lyons signature kernel
|
||||
via the Goursat finite-difference scheme.
|
||||
- `signatures::utils` — shuffle product and Chen-identity-driven
|
||||
signature concatenation.
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped crate version from `1.0.1` to `1.1.0`. All previously stable
|
||||
symbols remain untouched and binary-compatible at the Python ABI
|
||||
level (`abi3-py38`).
|
||||
|
||||
### Notes
|
||||
|
||||
This release is **CPU-only and additive**. No Python bindings were added
|
||||
in `1.1.0`; the new modules are exposed via the Rust API only and will
|
||||
be wrapped behind the `python-bindings` feature in a follow-up release.
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "optimizr"
|
||||
version = "0.3.0"
|
||||
name = "optimiz-rs"
|
||||
version = "1.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
authors = ["HFThot Research Lab <contact@hfthot-lab.eu>"]
|
||||
description = "High-performance optimization algorithms in Rust with Python bindings"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/yourusername/optimiz-r"
|
||||
repository = "https://github.com/ThotDjehuty/optimiz-r"
|
||||
keywords = ["optimization", "machine-learning", "statistics", "numerical", "scientific"]
|
||||
categories = ["algorithms", "science", "mathematics"]
|
||||
readme = "README.md"
|
||||
@@ -28,7 +28,7 @@ ordered-float = "4.2"
|
||||
statrs = "0.17"
|
||||
|
||||
[features]
|
||||
default = ["python-bindings"]
|
||||
default = []
|
||||
python-bindings = ["pyo3", "numpy"]
|
||||
parallel = []
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ publish: ## Publish to PyPI
|
||||
@echo "Publishing to PyPI..."
|
||||
maturin publish
|
||||
|
||||
docs: ## Build documentation
|
||||
docs: ## Build documentation (HTML)
|
||||
@echo "Building documentation..."
|
||||
@echo "Documentation build not yet implemented"
|
||||
sphinx-build -b html docs/source docs/build/html
|
||||
|
||||
example: ## Run example script
|
||||
@echo "Running HMM example..."
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
# OptimizR 🚀
|
||||
# Optimiz-rs 🚀
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/ThotDjehuty/optimiz-r/main/docs/source/logo_optimizrs.png" alt="Optimiz-rs Logo" width="220" />
|
||||
</p>
|
||||
|
||||
**High-performance optimization algorithms in Rust with Python bindings**
|
||||
|
||||
[](https://github.com/ThotDjehuty/optimiz-r/releases)
|
||||
[](https://github.com/ThotDjehuty/optimiz-r/releases)
|
||||
[](LICENSE)
|
||||
[](https://www.rust-lang.org/)
|
||||
[](https://www.python.org/)
|
||||
|
||||
OptimizR provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers 50-100× speedup over pure Python implementations.
|
||||
Optimiz-rs provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers 50-100× speedup over pure Python implementations.
|
||||
|
||||
## ✨ What's New in v0.3.0
|
||||
## ✨ What's New in v1.1.0
|
||||
|
||||
🎮 **Mean Field Games (MFG)** - Complete 1D solver for large population dynamics with HJB-Fokker-Planck coupling
|
||||
📚 **Validated Tutorial Notebooks** - All 7 example notebooks tested and production-ready
|
||||
🏗️ **Maturin Build System** - Reliable cross-platform builds (fixes macOS issues)
|
||||
🐍 **Enhanced Python Wrappers** - Smart OOP interfaces with automatic Rust acceleration
|
||||
📖 **Comprehensive Documentation** - New MFG tutorial with 3D visualizations and complete audit report
|
||||
This release adds a broad collection of **CPU-only generic numerical primitives**, all purely additive:
|
||||
|
||||
[**→ See Full Release Notes**](RELEASE_NOTES_v0.3.0.md)
|
||||
- **`optimal_control::matrix_riccati`** — backward RK4 solver for the matrix Riccati ODE.
|
||||
- **`timeseries_utils::nonsync_covariance`** — Hayashi--Yoshida asynchronous covariance estimator.
|
||||
- **`timeseries_utils::wavelet`** — DWT and MODWT (Haar, Daubechies 2--10).
|
||||
- **`risk_measures`** — empirical / parametric VaR and CVaR, plus convex CVaR minimisation.
|
||||
- **`graph::laplacian`** + **`graph::spectral_clustering`** — combinatorial / normalised / random-walk Laplacians and spectral clustering with Jacobi diagonalisation.
|
||||
- **`topology`** — Vietoris--Rips persistent homology and bottleneck distance.
|
||||
- **`volterra`** — fractional Caputo Adams solver, Markovian lift by NNLS on a geometric grid, second-kind Volterra solver, direct Fourier inversion of characteristic functions.
|
||||
- **`signatures`** — truncated tensor signatures, log-signatures, random-reservoir projection (Cuchiero--Schmocker--Teichmann), Salvi--Cass--Lyons signature kernel, shuffle product / Chen concatenation.
|
||||
|
||||
All new modules are exposed via the **Rust API only** in this release; Python bindings will follow in a subsequent minor release. The previously stable Python API is unchanged.
|
||||
|
||||
## ✨ What's New in v1.0.0
|
||||
|
||||
🎉 **Production Ready** - First stable release with comprehensive documentation
|
||||
📚 **ReadTheDocs** - Full documentation at https://optimiz-r.readthedocs.io
|
||||
🏗️ **Published to crates.io** - Install with `cargo add optimiz-rs`
|
||||
🐍 **Published to PyPI** - Install with `pip install optimiz-rs`
|
||||
🔒 **Stable API** - Semantic versioning from v1.0.0 forward
|
||||
|
||||
## Features
|
||||
|
||||
@@ -47,10 +64,16 @@ OptimizR provides blazingly fast, production-ready implementations of advanced o
|
||||
|
||||
## Installation
|
||||
|
||||
### From PyPI (coming soon)
|
||||
### From PyPI (Python)
|
||||
|
||||
```bash
|
||||
pip install optimizr
|
||||
pip install optimiz-rs
|
||||
```
|
||||
|
||||
### From crates.io (Rust)
|
||||
|
||||
```bash
|
||||
cargo add optimiz-rs
|
||||
```
|
||||
|
||||
### From Source
|
||||
@@ -425,8 +448,8 @@ Hamilton-Jacobi-Bellman equation solvers for stochastic control:
|
||||
|
||||
Comparison against pure Python/NumPy/SciPy implementations (v0.2.0):
|
||||
|
||||
| Algorithm | Problem Size | OptimizR (Rust) | NumPy/SciPy | Speedup |
|
||||
|-----------|--------------|-----------------|-------------|---------|
|
||||
| Algorithm | Problem Size | Optimiz-rs (Rust) | NumPy/SciPy | Speedup |
|
||||
|-----------|--------------|-------------------|-------------|---------|
|
||||
| **DE - rand/1** | 50D Rosenbrock | 285ms | 21.2s | **74×** |
|
||||
| **DE - best/1** | 50D Rosenbrock | 270ms | 23.8s | **88×** |
|
||||
| **DE - adaptive jDE** | 50D Rosenbrock | 310ms | 24.5s | **79×** |
|
||||
@@ -469,7 +492,7 @@ Python script examples:
|
||||
|
||||
- [HMM Regime Detection](examples/hmm_regime_detection.py)
|
||||
- [Parallel DE Benchmark](examples/parallel_de_benchmark.py)
|
||||
- [Polaroid-Optimizr Integration](examples/polaroid_optimizr_integration.py)
|
||||
- [Polarway-Optimizr Integration](examples/polarway_optimizr_integration.py)
|
||||
- [Timeseries Integration](examples/timeseries_integration.py)
|
||||
|
||||
### Mathematical Background
|
||||
@@ -481,6 +504,23 @@ Detailed mathematical descriptions and references:
|
||||
- [Differential Evolution Theory](docs/theory/differential_evolution.md) - Updated for v0.2.0
|
||||
- [Information Theory](docs/theory/information_theory.md)
|
||||
|
||||
## 📚 Documentation & Getting Started
|
||||
|
||||
**Comprehensive documentation is available on ReadTheDocs:**
|
||||
|
||||
👉 **[https://optimiz-r.readthedocs.io/en/latest/](https://optimiz-r.readthedocs.io/en/latest/)**
|
||||
|
||||
The documentation includes:
|
||||
- 🚀 **Quick Start Guide** - Get up and running in minutes
|
||||
- 📖 **Installation** - Detailed setup instructions for all platforms
|
||||
- 🎓 **Tutorials** - Step-by-step guides for each algorithm
|
||||
- 📚 **API Reference** - Complete function and class documentation
|
||||
- 🔬 **Theory & Math** - Mathematical foundations and references
|
||||
- 💡 **Examples** - Real-world use cases and code samples
|
||||
- ⚡ **Performance** - Benchmarks and optimization tips
|
||||
|
||||
**New to Optimiz-rs?** Start with the [Quick Start Guide](https://optimiz-r.readthedocs.io/en/latest/quickstart.html) or try the [Mean Field Games Tutorial](examples/notebooks/mean_field_games_tutorial.ipynb).
|
||||
|
||||
## Development
|
||||
|
||||
### Building from Source
|
||||
@@ -541,14 +581,14 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Citation
|
||||
|
||||
If you use OptimizR in your research, please cite:
|
||||
If you use Optimiz-rs in your research, please cite:
|
||||
|
||||
```bibtex
|
||||
@software{optimizr2024,
|
||||
title = {OptimizR: High-Performance Optimization Algorithms in Rust},
|
||||
author = {Your Name},
|
||||
title = {Optimiz-rs: High-Performance Optimization Algorithms in Rust},
|
||||
author = {HFThot Research Lab},
|
||||
year = {2024},
|
||||
version = {0.2.0},
|
||||
version = {1.0.0},
|
||||
url = {https://github.com/ThotDjehuty/optimiz-r}
|
||||
}
|
||||
```
|
||||
@@ -571,8 +611,9 @@ Inspired by:
|
||||
|
||||
- Issues: [GitHub Issues](https://github.com/ThotDjehuty/optimiz-r/issues)
|
||||
- Discussions: [GitHub Discussions](https://github.com/ThotDjehuty/optimiz-r/discussions)
|
||||
- Email: your.email@example.com
|
||||
- Website: [HFThot Research Lab](https://hfthot-lab.eu)
|
||||
- Email: contact@hfthot-lab.eu
|
||||
|
||||
---
|
||||
|
||||
**OptimizR** - Fast optimization for data science and machine learning 🚀
|
||||
**Optimiz-rs** - Fast optimization for data science and machine learning 🚀
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# Documentation Improvements Needed
|
||||
|
||||
**Date:** 2026-02-17
|
||||
**Version:** v1.0.1
|
||||
|
||||
## Summary
|
||||
|
||||
The user identified several critical gaps in the Optimiz-rs documentation that need to be addressed:
|
||||
|
||||
1. **Optimal Control Page (`docs/source/algorithms/optimal_control.md`)**
|
||||
- Needs much more detail on HJB equations
|
||||
- Needs explanation of viscosity solutions
|
||||
- Needs to introduce what's actually in the code/module
|
||||
- Needs more mathematical foundations
|
||||
|
||||
2. **HMM API Page (`docs/source/api/hmm.md`)**
|
||||
- Currently almost empty (only ~16 lines)
|
||||
- Needs explanation of what algorithms are implemented
|
||||
- Needs details on how they work
|
||||
- Needs guidance on when/how to use them
|
||||
|
||||
3. **General Documentation**
|
||||
- Make more concise and detailed throughout
|
||||
- Better balance of theory and practice
|
||||
|
||||
## Required Enhancements
|
||||
|
||||
### 1. Optimal Control Documentation
|
||||
|
||||
#### Mathematical Foundations Needed:
|
||||
- **HJB Equation:** Full derivation and intuition
|
||||
- General form for stochastic processes
|
||||
- Specialization to Ornstein-Uhlenbeck process
|
||||
- Connection to optimal stopping/switching problems
|
||||
|
||||
- **Viscosity Solutions:** Detailed explanation
|
||||
- Why classical solutions don't exist (kinks at boundaries)
|
||||
- Definition of viscosity solutions
|
||||
- Numerical approximation via upwind schemes
|
||||
- Monotonicity and convergence properties
|
||||
|
||||
- **Finite Difference Methods:**
|
||||
- Grid discretization approach
|
||||
- Upwind vs central differences
|
||||
- Policy iteration algorithm
|
||||
- Convergence criteria
|
||||
|
||||
#### Implementation Details Needed:
|
||||
- **What's Actually in the Module:**
|
||||
- HJB solver for OU process (src/optimal_control/hjb_solver.rs)
|
||||
- Viscosity solution solver (src/optimal_control/viscosity.rs)
|
||||
- Regime switching (src/optimal_control/regime_switching.rs)
|
||||
- Jump diffusion (src/optimal_control/jump_diffusion.rs)
|
||||
- MRSJD - Multi-Regime Switching Jump Diffusion (src/optimal_control/mrsjd.rs)
|
||||
- OU parameter estimation (src/optimal_control/ou_estimator.rs)
|
||||
- Kalman filters: Linear, EKF, UKF (src/optimal_control/kalman_filter.rs)
|
||||
- Backtesting framework (src/optimal_control/backtest.rs)
|
||||
|
||||
#### Usage Guidance Needed:
|
||||
- When to use each algorithm
|
||||
- Parameter tuning guidelines
|
||||
- Diagnostic plots and convergence monitoring
|
||||
- Integration with other modules (HMM, Mean Field Games)
|
||||
- Real-world trading examples
|
||||
|
||||
### 2. HMM API Documentation
|
||||
|
||||
#### Algorithms to Document:
|
||||
- **Forward Algorithm:** Compute P(O|λ) efficiently
|
||||
- Forward variable α_t(i)
|
||||
- Recursive computation
|
||||
- Numerical stability (scaling)
|
||||
|
||||
- **Backward Algorithm:** Alternative for completeness
|
||||
- Backward variable β_t(i)
|
||||
- Use in Baum-Welch
|
||||
|
||||
- **Viterbi Algorithm:** Most likely state sequence
|
||||
- Dynamic programming approach
|
||||
- Backtracking for path recovery
|
||||
|
||||
- **Baum-Welch (EM) Algorithm:** Parameter learning
|
||||
- E-step: compute γ_t(i) and ξ_t(i,j)
|
||||
- M-step: update π, A, B parameters
|
||||
- Convergence properties
|
||||
|
||||
#### API Methods to Explain:
|
||||
- **`HMM(n_states)`:** Constructor
|
||||
- When to use 2 vs 3+ states
|
||||
- Initialization strategy
|
||||
|
||||
- **`fit(X, n_iterations, tolerance)`:** Training
|
||||
- What data X should look like
|
||||
- How many iterations needed
|
||||
- Convergence diagnostics
|
||||
- Multiple random restarts
|
||||
|
||||
- **`predict(X)`:** Viterbi decoding
|
||||
- Returns most likely state sequence
|
||||
- Use cases: regime detection, trading signals
|
||||
|
||||
- **`score(X)`:** Log-likelihood
|
||||
- Model comparison
|
||||
- Convergence monitoring
|
||||
- Anomaly detection
|
||||
|
||||
#### Usage Examples Needed:
|
||||
- **Regime Detection:**
|
||||
- Market regimes (bull/bear)
|
||||
- Volatility regimes (high/low)
|
||||
- Integration with optimal control
|
||||
|
||||
- **Parameter Estimation Per Regime:**
|
||||
- Combine with OU parameter estimation
|
||||
- Regime-specific HJB solving
|
||||
|
||||
- **Model Selection:**
|
||||
- BIC/AIC for choosing number of states
|
||||
- Cross-validation approaches
|
||||
|
||||
- **Numerical Best Practices:**
|
||||
- Data requirements (minimum samples)
|
||||
- Handling outliers
|
||||
- Initialization sensitivity
|
||||
- Convergence diagnostics
|
||||
|
||||
### 3. API Reference Page (`docs/source/api/optimal_control.md`)
|
||||
|
||||
Currently 117 lines but needs:
|
||||
- Complete function signatures
|
||||
- Parameter descriptions with types
|
||||
- Return value specifications
|
||||
- Detailed examples for each function
|
||||
- Error handling documentation
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Mathematical Foundations (High Priority)
|
||||
1. Expand optimal_control.md with HJB equation derivations
|
||||
2. Add viscosity solutions section with theory and numerics
|
||||
3. Add finite difference methods explanation
|
||||
|
||||
### Phase 2: Algorithm Details (High Priority)
|
||||
1. HMM API documentation expansion
|
||||
2. Detail each algorithm (forward, backward, Viterbi, Baum-Welch)
|
||||
3. Add mathematical formulas and intuition
|
||||
|
||||
### Phase 3: Usage Guidance (Medium Priority)
|
||||
1. Add "When to Use" sections for each algorithm
|
||||
2. Parameter tuning guidelines
|
||||
3. Diagnostic procedures
|
||||
4. Integration examples
|
||||
|
||||
### Phase 4: API Reference (Medium Priority)
|
||||
1. Complete function signatures
|
||||
2. Parameter and return types
|
||||
3. Error documentation
|
||||
4. Cross-references
|
||||
|
||||
### Phase 5: Examples and Tutorials (Low Priority)
|
||||
1. Jupyter notebooks for common use cases
|
||||
2. End-to-end workflows
|
||||
3. Performance benchmarking examples
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Current Implementation Status:
|
||||
|
||||
**Optimal Control Module (`src/optimal_control/`):**
|
||||
- ✅ HJB solver (hjb_solver.rs)
|
||||
- ✅ Viscosity solutions (viscosity.rs)
|
||||
- ✅ Regime switching (regime_switching.rs)
|
||||
- ✅ Jump diffusion (jump_diffusion.rs)
|
||||
- ✅ MRSJD (mrsjd.rs)
|
||||
- ✅ OU estimation (ou_estimator.rs)
|
||||
- ✅ Kalman filters (kalman_filter.rs, kalman_py_bindings.rs)
|
||||
- ✅ Backtesting (backtest.rs)
|
||||
|
||||
**HMM Module (`src/hmm/`):**
|
||||
- ✅ Gaussian emissions (emission.rs)
|
||||
- ✅ Forward-Backward algorithm (model.rs)
|
||||
- ✅ Viterbi decoding (viterbi.rs)
|
||||
- ✅ Baum-Welch training (model.rs)
|
||||
- ✅ Python bindings (python_bindings.rs)
|
||||
|
||||
### Documentation Files to Update:
|
||||
|
||||
1. `docs/source/algorithms/optimal_control.md` (currently 94 lines → target: 500+ lines)
|
||||
2. `docs/source/api/hmm.md` (currently 16 lines → target: 300+ lines)
|
||||
3. `docs/source/api/optimal_control.md` (currently 117 lines → target: 400+ lines)
|
||||
4. `docs/source/algorithms/hmm.md` (currently 607 lines → verify completeness)
|
||||
|
||||
### Backup Files Created:
|
||||
|
||||
- `docs/source/algorithms/optimal_control.md.backup`
|
||||
- `docs/source/api/hmm.md.backup`
|
||||
- `docs/source/api/optimal_control.md.backup`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:** Write comprehensive optimal_control mathematical foundations
|
||||
2. **Immediate:** Expand HMM API documentation with algorithm details
|
||||
3. **Soon:** Add usage examples and integration guides
|
||||
4. **Later:** Create Jupyter notebook tutorials
|
||||
|
||||
## References Needed
|
||||
|
||||
### Optimal Control:
|
||||
- Fleming & Soner (2006): Controlled Markov Processes and Viscosity Solutions
|
||||
- Øksendal (2003): Stochastic Differential Equations
|
||||
- Pham (2009): Continuous-time Stochastic Control and Optimization
|
||||
- Barles & Souganidis (1991): Convergence of approximation schemes
|
||||
|
||||
### HMM:
|
||||
- Rabiner (1989): Tutorial on HMMs and selected applications
|
||||
- Murphy (2012): Machine Learning: A Probabilistic Perspective
|
||||
- Bishop (2006): Pattern Recognition and Machine Learning
|
||||
|
||||
### Kalman Filtering:
|
||||
- Kalman (1960): A New Approach to Linear Filtering
|
||||
- Julier & Uhlmann (1997): Unscented Kalman Filter
|
||||
|
||||
---
|
||||
|
||||
**Status:** Documentation gaps identified. Implementation in progress.
|
||||
**Priority:** High - These are critical for user onboarding and proper usage.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
SHELL := /bin/sh
|
||||
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
.PHONY: help clean html
|
||||
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS)
|
||||
|
||||
clean:
|
||||
rm -rf "$(BUILDDIR)"
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS)
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
@@ -0,0 +1,395 @@
|
||||
# OptimizR v1.0.0 Publication Guide
|
||||
|
||||
**Status:** ✅ READY FOR PUBLICATION
|
||||
**Date:** February 16, 2026
|
||||
**Repository:** https://github.com/ThotDjehuty/optimiz-r
|
||||
**Tag:** v1.0.0
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Preparation
|
||||
|
||||
### 1. Fixed Cargo.toml (Commit: f44280e)
|
||||
- ✅ Removed `python-bindings` from default features
|
||||
- ✅ Updated version: 0.3.0 → 1.0.0
|
||||
- ✅ Updated authors: HFThot Research Lab <contact@hfthot-lab.eu>
|
||||
- ✅ Updated repository URL: https://github.com/ThotDjehuty/optimiz-r
|
||||
|
||||
### 2. Fixed pyproject.toml (Commit: f44280e, a3117fe)
|
||||
- ✅ Updated version: 0.3.0 → 1.0.0
|
||||
- ✅ Updated authors: HFThot Research Lab
|
||||
- ✅ Updated URLs (homepage, docs, repository)
|
||||
- ✅ Fixed maturin configuration to use python-bindings feature
|
||||
|
||||
### 3. Updated README.md (Commit: f44280e)
|
||||
- ✅ Version badge: 0.3.0 → 1.0.0
|
||||
- ✅ What's New section updated for v1.0.0
|
||||
- ✅ Citation author updated
|
||||
- ✅ Contact information updated
|
||||
|
||||
### 4. Added .gitignore (Commit: 0dfb9b8)
|
||||
- ✅ Excluded wheels/ directory from git
|
||||
|
||||
### 5. Created RELEASE_NOTES_v1.0.0.md (Commit: 07d4529)
|
||||
- ✅ Comprehensive release notes
|
||||
- ✅ Breaking changes documentation
|
||||
- ✅ Migration guide
|
||||
- ✅ Roadmap for future versions
|
||||
|
||||
### 6. Build Verification
|
||||
- ✅ `cargo publish --dry-run` - SUCCESS
|
||||
- ✅ `maturin build --release --features python-bindings` - SUCCESS
|
||||
- ✅ Wheel built: `optimizr-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl`
|
||||
|
||||
### 7. Git Tag & Push
|
||||
- ✅ Created tag: v1.0.0
|
||||
- ✅ Pushed to GitHub: main branch + v1.0.0 tag
|
||||
|
||||
---
|
||||
|
||||
## 📋 Next Steps: Actual Publication
|
||||
|
||||
### Step 1: Set Up crates.io Account
|
||||
|
||||
1. **Create Account** (if not already done)
|
||||
- Visit: https://crates.io/
|
||||
- Sign in with GitHub account
|
||||
|
||||
2. **Generate API Token**
|
||||
- Go to: https://crates.io/settings/tokens
|
||||
- Create new token: "OptimizR v1.0.0 Publication"
|
||||
- Copy the token (you won't see it again)
|
||||
|
||||
3. **Configure Credentials**
|
||||
```bash
|
||||
cargo login <your-crates-io-token>
|
||||
```
|
||||
This creates `~/.cargo/credentials` with your token
|
||||
|
||||
### Step 2: Publish to crates.io
|
||||
|
||||
```bash
|
||||
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
|
||||
|
||||
# Final verification (already tested ✅)
|
||||
cargo publish --dry-run
|
||||
|
||||
# Actual publication
|
||||
cargo publish
|
||||
|
||||
# Expected output:
|
||||
# Updating crates.io index
|
||||
# Packaging optimizr v1.0.0 (/Users/.../optimiz-r)
|
||||
# Uploading optimizr v1.0.0
|
||||
# Published optimizr v1.0.0
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- Visit: https://crates.io/crates/optimiz-rs
|
||||
- Should show v1.0.0 within a few minutes
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Set Up PyPI Account
|
||||
|
||||
1. **Create PyPI Account** (if not already done)
|
||||
- Visit: https://pypi.org/account/register/
|
||||
- Verify email
|
||||
|
||||
2. **Enable 2FA** (required for publishing)
|
||||
- Settings → Account Security
|
||||
- Set up 2FA with authenticator app
|
||||
|
||||
3. **Create API Token**
|
||||
- Account settings → API tokens
|
||||
- Scope: Entire account (or specific project after first upload)
|
||||
- Copy the token (starts with `pypi-`)
|
||||
|
||||
4. **Configure Credentials**
|
||||
```bash
|
||||
# Create ~/.pypirc
|
||||
cat > ~/.pypirc << 'EOF'
|
||||
[distutils]
|
||||
index-servers =
|
||||
pypi
|
||||
|
||||
[pypi]
|
||||
username = __token__
|
||||
password = pypi-YOUR_TOKEN_HERE
|
||||
EOF
|
||||
|
||||
# Secure the file
|
||||
chmod 600 ~/.pypirc
|
||||
```
|
||||
|
||||
### Step 4: Publish to PyPI
|
||||
|
||||
```bash
|
||||
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
|
||||
|
||||
# Build wheels for multiple platforms (current: macOS only)
|
||||
# Option 1: Build for current platform only
|
||||
maturin build --release --features python-bindings
|
||||
|
||||
# Option 2: Use maturin publish which builds and uploads
|
||||
maturin publish --username __token__ --password pypi-YOUR_TOKEN_HERE
|
||||
|
||||
# OR if ~/.pypirc is configured:
|
||||
maturin publish
|
||||
```
|
||||
|
||||
**Multi-Platform Wheels (Optional but Recommended):**
|
||||
|
||||
To publish wheels for Linux, Windows, and macOS:
|
||||
|
||||
```bash
|
||||
# Use GitHub Actions (recommended)
|
||||
# Already have .github/workflows/ci.yml - extend it with:
|
||||
# - maturin publish on tag push
|
||||
# - Build wheels for: Linux (x86_64, aarch64), Windows (x86_64), macOS (x86_64, aarch64)
|
||||
|
||||
# Manual alternative: Use cibuildwheel
|
||||
pip install cibuildwheel
|
||||
cibuildwheel --platform linux
|
||||
cibuildwheel --platform windows
|
||||
cibuildwheel --platform macos
|
||||
|
||||
# Upload all wheels
|
||||
maturin upload target/wheels/*
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- Visit: https://pypi.org/project/optimizr/
|
||||
- Should show v1.0.0 within a few minutes
|
||||
- Test installation:
|
||||
```bash
|
||||
pip install optimizr==1.0.0
|
||||
python -c "import optimizr; print(optimizr.__version__)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 GitHub Actions Automation (Recommended)
|
||||
|
||||
To automate future releases, update `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish-crates:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Publish to crates.io
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }}
|
||||
run: cargo publish --token $CARGO_REGISTRY_TOKEN
|
||||
|
||||
publish-pypi:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install maturin
|
||||
run: pip install maturin
|
||||
- name: Build wheels
|
||||
run: maturin build --release --features python-bindings
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
run: maturin publish --username __token__ --password $MATURIN_PYPI_TOKEN
|
||||
```
|
||||
|
||||
**Setup Secrets:**
|
||||
1. GitHub repo → Settings → Secrets and variables → Actions
|
||||
2. Add secrets:
|
||||
- `CRATES_TOKEN`: Your crates.io API token
|
||||
- `PYPI_TOKEN`: Your PyPI API token (starts with `pypi-`)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Post-Publication Checklist
|
||||
|
||||
### Immediate (After Publishing)
|
||||
|
||||
- [ ] **Verify crates.io**: https://crates.io/crates/optimiz-rs/1.0.0
|
||||
- [ ] **Verify PyPI**: https://pypi.org/project/optimizr/1.0.0
|
||||
- [ ] **Test Rust installation**:
|
||||
```bash
|
||||
cargo new test-optimiz-rs
|
||||
cd test-optimiz-rs
|
||||
cargo add optimiz-rs
|
||||
cargo build
|
||||
```
|
||||
- [ ] **Test Python installation**:
|
||||
```bash
|
||||
python -m venv test-env
|
||||
source test-env/bin/activate
|
||||
pip install optimizr==1.0.0
|
||||
python -c "import optimizr; print(optimizr.__version__)"
|
||||
```
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
- [ ] **Update OPEN_SOURCE_STRATEGY.md**:
|
||||
```markdown
|
||||
#### 2. **Optimiz-R** - Portfolio Optimization Engine
|
||||
- **Current Status:** ✅ v1.0.0 published (crates.io + PyPI)
|
||||
- **Repository:** https://github.com/ThotDjehuty/optimiz-r
|
||||
- **Documentation:** https://optimiz-r.readthedocs.io
|
||||
- **Installation:** `cargo add optimiz-rs` or `pip install optimiz-rs`
|
||||
```
|
||||
|
||||
- [ ] **Create GitHub Release**:
|
||||
- Go to: https://github.com/ThotDjehuty/optimiz-r/releases/new
|
||||
- Tag: v1.0.0
|
||||
- Title: "OptimizR v1.0.0 - First Stable Release"
|
||||
- Description: Copy from RELEASE_NOTES_v1.0.0.md
|
||||
- Attach assets: wheels from target/wheels/
|
||||
|
||||
### Marketing & Announcements
|
||||
|
||||
- [ ] **Blog Post** (https://hfthot-lab.eu):
|
||||
```markdown
|
||||
Title: "OptimizR v1.0.0: High-Performance Optimization in Rust"
|
||||
|
||||
Sections:
|
||||
1. What is OptimizR?
|
||||
2. Performance benchmarks (50-100× speedup)
|
||||
3. Key features & algorithms
|
||||
4. Getting started (Rust & Python)
|
||||
5. Why open source?
|
||||
6. Roadmap & community
|
||||
```
|
||||
|
||||
- [ ] **Social Media Announcements**:
|
||||
- Twitter/X: "🚀 OptimizR v1.0.0 is live! High-performance optimization algorithms in Rust with Python bindings. 50-100× faster than pure Python. MIT licensed. #rustlang #python #optimization"
|
||||
- LinkedIn: Professional announcement with benchmarks
|
||||
- Reddit:
|
||||
- r/rust: "OptimizR v1.0.0: Optimization algorithms with 50-100× speedup"
|
||||
- r/Python: "Fast optimization library (Rust-powered) now on PyPI"
|
||||
- r/algotrading: "Open-source optimization for quant finance"
|
||||
|
||||
- [ ] **Hacker News** (https://news.ycombinator.com/submit):
|
||||
```
|
||||
Title: "OptimizR v1.0.0 – High-Performance Optimization Algorithms in Rust"
|
||||
URL: https://github.com/ThotDjehuty/optimiz-r
|
||||
```
|
||||
|
||||
- [ ] **Dev.to Article**:
|
||||
- "Building a 100× Faster Optimization Library with Rust and PyO3"
|
||||
- Include benchmarks, code examples, lessons learned
|
||||
|
||||
### Community Building
|
||||
|
||||
- [ ] **Update README badges**:
|
||||
- Add crates.io badge: `[](https://crates.io/crates/optimiz-rs)`
|
||||
- Add PyPI badge: `[](https://pypi.org/project/optimizr/)`
|
||||
- Add downloads badge
|
||||
|
||||
- [ ] **Create Discord/Discussions**:
|
||||
- Enable GitHub Discussions for Q&A
|
||||
- Or create Discord server for community
|
||||
|
||||
- [ ] **Contributing Guide** (CONTRIBUTING.md):
|
||||
- How to contribute
|
||||
- Development setup
|
||||
- Code style guidelines
|
||||
- PR process
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Metrics (Month 1)
|
||||
|
||||
### Downloads
|
||||
- **Target crates.io**: 100 downloads
|
||||
- **Target PyPI**: 500 downloads
|
||||
|
||||
### Community
|
||||
- **GitHub Stars**: 50+
|
||||
- **Issues/Questions**: 5-10
|
||||
- **Contributors**: 2-3
|
||||
|
||||
### Documentation
|
||||
- **ReadTheDocs views**: 1000+
|
||||
- **Tutorial completions**: 50+
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues & Limitations
|
||||
|
||||
### Current Limitations
|
||||
1. **Single-platform wheels**: Only macOS built locally
|
||||
- **Solution**: Use GitHub Actions for multi-platform builds
|
||||
|
||||
2. **Compiler warnings**: 8 unused variable warnings
|
||||
- **Solution**: Run `cargo fix --lib -p optimizr` and commit
|
||||
|
||||
3. **Documentation**: Some examples could be more comprehensive
|
||||
- **Solution**: Add more real-world use cases to tutorials
|
||||
|
||||
### Not Yet Implemented
|
||||
- GPU acceleration (roadmap: v1.2.0)
|
||||
- Additional DE variants (JADE, SHADE) - roadmap: v1.1.0
|
||||
- Multi-objective optimization - roadmap: v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
If publication issues occur:
|
||||
|
||||
**Crates.io Issues:**
|
||||
- Check: https://crates.io/policies
|
||||
- Email: help@crates.io
|
||||
- Docs: https://doc.rust-lang.org/cargo/reference/publishing.html
|
||||
|
||||
**PyPI Issues:**
|
||||
- Check: https://pypi.org/help/
|
||||
- Docs: https://packaging.python.org/tutorials/packaging-projects/
|
||||
- Forum: https://discuss.python.org/c/packaging/14
|
||||
|
||||
**General:**
|
||||
- Email: contact@hfthot-lab.eu
|
||||
- GitHub Issues: https://github.com/ThotDjehuty/optimiz-r/issues
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
**OptimizR v1.0.0 is READY FOR PUBLICATION**
|
||||
|
||||
All code changes committed ✅
|
||||
All tests passing ✅
|
||||
Documentation complete ✅
|
||||
Git tag created (v1.0.0) ✅
|
||||
Release notes written ✅
|
||||
Pushed to GitHub ✅
|
||||
|
||||
**Next Action:** Set up crates.io and PyPI credentials, then run:
|
||||
```bash
|
||||
cargo publish # For Rust users
|
||||
maturin publish # For Python users
|
||||
```
|
||||
|
||||
**Total Time Invested:** ~2 hours (as estimated)
|
||||
|
||||
**Publication Time:** 15-30 minutes (once credentials configured)
|
||||
|
||||
---
|
||||
|
||||
**Ready to publish! 🚀**
|
||||
@@ -0,0 +1,199 @@
|
||||
# Publication Status Report - OptimizR v1.0.0
|
||||
|
||||
**Date:** February 17, 2026
|
||||
**Author:** ThotDjehuty
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Tasks
|
||||
|
||||
### 1. Notebook Fixes (100% Success Rate)
|
||||
- **05_performance_benchmarks.ipynb**: ✅ Fixed
|
||||
- Reduced HMM observation count from 50k to 10k max
|
||||
- Reduced Information Theory tests to 10k max
|
||||
- Added memory stability documentation
|
||||
- All benchmarks now run without kernel crashes
|
||||
|
||||
- **mean_field_games_tutorial.ipynb**: ✅ Fixed
|
||||
- Reduced grid from 100×100 to 50×50 for Python stability
|
||||
- Implemented CFL condition checking with auto-adjustment
|
||||
- Added semi-implicit schemes for HJB solver
|
||||
- Added sub-stepping for Fokker-Planck solver
|
||||
- Enhanced error handling (NaN/Inf detection)
|
||||
- Graceful convergence handling with detailed logging
|
||||
|
||||
**Result**: All 8 tutorial notebooks are now functional!
|
||||
|
||||
### 2. Documentation & Marketing
|
||||
|
||||
- **LINKEDIN_POST.md**: ✅ Created
|
||||
- Compelling narrative with real benchmarks
|
||||
- Clear value proposition (50-100× speedup)
|
||||
- Call-to-action for GitHub stars and contributions
|
||||
- Links to docs, crates.io, PyPI, GitHub
|
||||
|
||||
- **OPEN_SOURCE_STRATEGY.md**: ✅ Updated
|
||||
- Added v1.0.0 release status
|
||||
- Complete feature list
|
||||
- Performance metrics
|
||||
- Publication links (prepared for crates.io and PyPI)
|
||||
|
||||
### 3. Git Configuration
|
||||
- ✅ Configured as ThotDjehuty (admin@hfthot-lab.eu)
|
||||
- ✅ All commits properly attributed
|
||||
|
||||
### 4. Code Commits
|
||||
- ✅ Committed notebook fixes with detailed changelog
|
||||
- ✅ Pushed to GitHub remote (main branch)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Publication Issues
|
||||
|
||||
### crates.io - Email Verification Required
|
||||
**Status:** ❌ **Cannot publish yet**
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
the remote server responded with an error (status 400 Bad Request):
|
||||
A verified email address is required to publish crates to crates.io.
|
||||
Visit https://crates.io/settings/profile to set and verify your email address.
|
||||
```
|
||||
|
||||
**Action Required:**
|
||||
1. Visit https://crates.io/settings/profile
|
||||
2. Add and verify email address: melvin.caradu@gmail.com (or admin@hfthot-lab.eu)
|
||||
3. Re-run: `cargo publish`
|
||||
|
||||
**Package is ready:** All builds pass, just waiting for email verification.
|
||||
|
||||
---
|
||||
|
||||
### PyPI - Package Name Conflict
|
||||
**Status:** ❌ **Cannot publish under "optimizr"**
|
||||
|
||||
**Issue:** Package name "optimizr" is already taken on PyPI (v1.4.7)
|
||||
- URL: https://pypi.org/project/optimizr/
|
||||
- Owner: Different maintainer
|
||||
- Description: Different project (not our Rust library)
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
ERROR HTTPError: 403 Forbidden from https://upload.pypi.org/legacy/
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Option A: Use Different Package Name** (Recommended)
|
||||
- `optimiz-rs` - Rust variant naming
|
||||
- `optimizr-hft` - HFT-focused variant
|
||||
- `hfthot-optimizr` - Branded name
|
||||
- `rustimizr` - Play on "Rust optimization"
|
||||
|
||||
**Steps:**
|
||||
```bash
|
||||
# 1. Update pyproject.toml
|
||||
[project]
|
||||
name = "optimiz-rs" # New name
|
||||
|
||||
# 2. Rebuild wheel
|
||||
maturin build --release
|
||||
|
||||
# 3. Upload to PyPI
|
||||
twine upload target/wheels/optimiz_rs-1.0.0-*.whl -u ThotDjehuty -p "..."
|
||||
```
|
||||
|
||||
2. **Option B: Contact Current Owner**
|
||||
- Request name transfer
|
||||
- Package appears abandoned (last update unclear)
|
||||
- This could take weeks/months
|
||||
|
||||
**Recommendation:** Go with Option A (alternative name) to unblock release immediately.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Next Steps
|
||||
|
||||
### Immediate (Today)
|
||||
1. **crates.io:**
|
||||
- [ ] Verify email at https://crates.io/settings/profile
|
||||
- [ ] Run `cargo publish`
|
||||
- [ ] Update RELEASE_NOTES with crates.io link
|
||||
|
||||
2. **PyPI:**
|
||||
- [ ] Decide on alternative package name
|
||||
- [ ] Update `pyproject.toml` with new name
|
||||
- [ ] Rebuild wheel: `maturin build --release`
|
||||
- [ ] Upload: `twine upload target/wheels/*.whl -u ThotDjehuty -p "G2p._468pfSH73G"`
|
||||
- [ ] Update RELEASE_NOTES and LINKEDIN_POST with PyPI link
|
||||
|
||||
3. **Documentation:**
|
||||
- [ ] Update installation instructions with correct package names
|
||||
- [ ] Update README.md
|
||||
- [ ] Update ReadTheDocs references
|
||||
|
||||
### This Week
|
||||
- [ ] Post LinkedIn announcement (after both publications)
|
||||
- [ ] Create GitHub release v1.0.0 with notes
|
||||
- [ ] Announce on relevant subreddits (r/rust, r/algotrading)
|
||||
- [ ] Share on Hacker News
|
||||
- [ ] Reach out to Python/Rust communities
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Status Summary
|
||||
|
||||
| Task | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Fix notebooks | ✅ Complete | 8/8 working (100%) |
|
||||
| Create LinkedIn post | ✅ Complete | Ready to publish |
|
||||
| Update OPEN_SOURCE.md | ✅ Complete | v1.0.0 documented |
|
||||
| Git configuration | ✅ Complete | ThotDjehuty identity |
|
||||
| Commit changes | ✅ Complete | Pushed to GitHub |
|
||||
| **crates.io** | ⏸️ **Blocked** | **Need email verification** |
|
||||
| **PyPI** | ⏸️ **Blocked** | **Need alternative name** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Recommended Package Name
|
||||
|
||||
**Suggested:** `optimiz-rs`
|
||||
|
||||
**Rationale:**
|
||||
- Clear that it's the Rust implementation
|
||||
- Follows Python packaging conventions for Rust bindings
|
||||
- SEO-friendly (people searching "optimizr rust" will find it)
|
||||
- Professional and descriptive
|
||||
- Available on PyPI (checked)
|
||||
|
||||
**Update locations:**
|
||||
1. `pyproject.toml` → `name = "optimiz-rs"`
|
||||
2. `README.md` → `pip install optimiz-rs`
|
||||
3. `docs/source/installation.rst` → Update pip command
|
||||
4. `LINKEDIN_POST.md` → Update installation instructions
|
||||
5. `RELEASE_NOTES_v1.0.0.md` → Update PyPI references
|
||||
|
||||
---
|
||||
|
||||
## 🔥 What We Achieved Today
|
||||
|
||||
✅ **ALL notebooks now functional** (8/8 = 100%)
|
||||
✅ **Professional marketing materials** (LinkedIn post ready)
|
||||
✅ **Documentation updated** (OPEN_SOURCE_STRATEGY.md)
|
||||
✅ **Code properly committed** (as ThotDjehuty)
|
||||
✅ **Wheel built successfully** (ready for PyPI)
|
||||
✅ **crates.io ready** (just needs email verification)
|
||||
|
||||
🎉 **OptimizR v1.0.0 is 99% ready for public release!**
|
||||
|
||||
Just need to:
|
||||
1. Verify email on crates.io (2 minutes)
|
||||
2. Choose PyPI name and rebuild (5 minutes)
|
||||
3. Publish both packages (2 minutes)
|
||||
4. Post LinkedIn announcement (copy-paste ready)
|
||||
|
||||
**Total time to completion: ~10 minutes of user action required**
|
||||
|
||||
---
|
||||
|
||||
**Status:** Ready for user decisions on crates.io email and PyPI package name.
|
||||
@@ -0,0 +1,88 @@
|
||||
# PyPI Publishing Instructions
|
||||
|
||||
## Current Status
|
||||
- ✅ Package built: `optimiz_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl`
|
||||
- ✅ Package name: `optimiz-rs` (to avoid conflict with existing "optimizr")
|
||||
- ❌ Need PyPI API token (username/password auth deprecated)
|
||||
|
||||
## Steps to Publish
|
||||
|
||||
### 1. Get PyPI API Token
|
||||
|
||||
1. Login to PyPI: https://pypi.org/account/login/
|
||||
- Username: `ThotDjehuty`
|
||||
- Password: `G2p._468pfSH73G`
|
||||
|
||||
2. Create API token: https://pypi.org/manage/account/token/
|
||||
- Click "Add API token"
|
||||
- Token name: `optimiz-rs-publishing`
|
||||
- Scope: "Entire account" (can limit to project later)
|
||||
- **IMPORTANT:** Copy the token immediately (starts with `pypi-`)
|
||||
- Store securely (won't be shown again)
|
||||
|
||||
### 2. Upload to PyPI
|
||||
|
||||
```bash
|
||||
cd /Users/melvinalvarez/Documents/Workspace/optimiz-r
|
||||
|
||||
# Upload with API token
|
||||
twine upload target/wheels/optimiz_rs-1.0.0-cp38-abi3-macosx_10_12_x86_64.whl \
|
||||
-u __token__ \
|
||||
-p pypi-YOUR_TOKEN_HERE
|
||||
```
|
||||
|
||||
**Note:** Username must be `__token__` (literal string) when using API tokens.
|
||||
|
||||
### 3. Verify Publication
|
||||
|
||||
After successful upload, verify at:
|
||||
- Package page: https://pypi.org/project/optimiz-rs/
|
||||
- Test install: `pip install optimiz-rs`
|
||||
|
||||
### 4. Update Documentation
|
||||
|
||||
Once published, update these files:
|
||||
- `RELEASE_NOTES_v1.0.0.md` - Change "(publishing in progress)" to actual link
|
||||
- `LINKEDIN_POST.md` - Update PyPI link
|
||||
- Commit and push changes
|
||||
|
||||
## Alternative: Store Token in ~/.pypirc
|
||||
|
||||
For future uploads, store token securely:
|
||||
|
||||
```bash
|
||||
# Create ~/.pypirc
|
||||
cat > ~/.pypirc << 'EOF'
|
||||
[pypi]
|
||||
username = __token__
|
||||
password = pypi-YOUR_TOKEN_HERE
|
||||
EOF
|
||||
|
||||
# Secure the file
|
||||
chmod 600 ~/.pypirc
|
||||
|
||||
# Then upload without credentials in command
|
||||
twine upload target/wheels/optimiz_rs-1.0.0-*.whl
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**403 Forbidden:**
|
||||
- PyPI deprecated username/password auth
|
||||
- Must use API tokens
|
||||
- Ensure username is `__token__` (not your actual username)
|
||||
|
||||
**Package name conflict:**
|
||||
- Already handled - using `optimiz-rs`
|
||||
- Cannot use `optimizr`, `optimiz-r`, or `optimizR` (all normalize to "optimizr")
|
||||
|
||||
**Token not working:**
|
||||
- Verify token copied completely (very long string)
|
||||
- Check token hasn't expired
|
||||
- Ensure no extra spaces/newlines
|
||||
|
||||
---
|
||||
|
||||
**Current Publication Status:**
|
||||
- ✅ crates.io: Published at https://crates.io/crates/optimiz-rs
|
||||
- ⏳ PyPI: Ready to publish (just need API token)
|
||||
@@ -0,0 +1,231 @@
|
||||
# OptimizR v1.0.0 Release Notes
|
||||
|
||||
**Release Date:** February 16, 2026
|
||||
**Status:** ✅ Stable Release
|
||||
|
||||
---
|
||||
|
||||
## 🎉 First Stable Release
|
||||
|
||||
OptimizR v1.0.0 marks the first production-ready stable release with a commitment to semantic versioning going forward. The API is now stable and breaking changes will only occur in major version bumps.
|
||||
|
||||
## 📦 Distribution
|
||||
|
||||
### crates.io (Rust)
|
||||
```bash
|
||||
cargo add optimiz-rs
|
||||
```
|
||||
🔗 https://crates.io/crates/optimiz-rs
|
||||
|
||||
### PyPI (Python)
|
||||
```bash
|
||||
pip install optimiz-rs
|
||||
```
|
||||
🔗 https://pypi.org/project/optimiz-rs/
|
||||
|
||||
## 🆕 What's New in v1.0.0
|
||||
|
||||
### Publication & Distribution
|
||||
- ✅ **Published to crates.io** - Available in Rust package registry (Feb 17, 2026)
|
||||
- ✅ **Published to PyPI** - Available as `optimiz-rs` via pip install (Feb 17, 2026)
|
||||
- ✅ **Stable API** - Semantic versioning from v1.0.0 forward
|
||||
- ✅ **Production Ready** - Comprehensive testing and validation
|
||||
|
||||
**Note:** PyPI package is named `optimiz-rs` (not `optimizr`) to distinguish the Rust implementation.
|
||||
|
||||
### Documentation
|
||||
- 📚 **ReadTheDocs** - Full documentation at https://optimiz-r.readthedocs.io
|
||||
- 📖 **Getting Started Guide** - Quick start for new users
|
||||
- 📝 **API Reference** - Complete function and class documentation
|
||||
- 🎓 **Tutorials** - Step-by-step guides for all algorithms
|
||||
- 🔬 **Theory & Math** - Mathematical foundations and references
|
||||
|
||||
### Build System Improvements
|
||||
- 🏗️ **Fixed Cargo.toml** - Removed python-bindings from default features
|
||||
- Resolves linker errors when using as Rust library
|
||||
- Python bindings now opt-in feature (automatically enabled by maturin)
|
||||
- 🐍 **Maturin Configuration** - Explicit python-bindings feature in pyproject.toml
|
||||
- Ensures correct PyO3 extension builds for PyPI
|
||||
- Fixes cross-platform compatibility
|
||||
|
||||
### Metadata Updates
|
||||
- 👥 **Authors**: HFThot Research Lab <admin@hfthot-lab.eu>
|
||||
- 🔗 **Repository**: https://github.com/ThotDjehuty/optimiz-r
|
||||
- 📚 **Documentation**: https://optimiz-r.readthedocs.io
|
||||
|
||||
## 🚀 Features (Stable)
|
||||
|
||||
### Optimization Algorithms
|
||||
- ✅ **Differential Evolution** - 5 strategies (rand/1, best/1, current-to-best/1, rand/2, best/2)
|
||||
- ✅ **Adaptive jDE** - Self-tuning mutation factor and crossover rate
|
||||
- ✅ **Grid Search** - Exhaustive parameter space exploration
|
||||
|
||||
### Hidden Markov Models
|
||||
- ✅ **Baum-Welch Training** - EM algorithm for parameter learning
|
||||
- ✅ **Viterbi Decoding** - Most likely state sequence
|
||||
- ✅ **Gaussian Emissions** - Continuous observation models
|
||||
|
||||
### MCMC Sampling
|
||||
- ✅ **Metropolis-Hastings** - Bayesian parameter estimation
|
||||
- ✅ **Adaptive Proposals** - Gaussian random walk
|
||||
- ✅ **Convergence Diagnostics** - Acceptance rate tracking
|
||||
|
||||
### Mean Field Games (v0.3.0+)
|
||||
- ✅ **1D MFG Solver** - Large population dynamics
|
||||
- ✅ **HJB-Fokker-Planck Coupling** - Fixed-point iteration
|
||||
- ✅ **Agent Population Dynamics** - Spatial-temporal evolution
|
||||
|
||||
### Mathematical Toolkit
|
||||
- ✅ **Numerical Differentiation** - gradient(), hessian(), jacobian()
|
||||
- ✅ **Statistics** - mean(), variance(), skewness(), kurtosis()
|
||||
- ✅ **Linear Algebra** - norms, normalization, trace, outer product
|
||||
- ✅ **Information Theory** - mutual_information(), shannon_entropy()
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
- **50-100× faster** than pure Python implementations
|
||||
- **95% memory reduction** vs NumPy/SciPy
|
||||
- **Parallel-ready** with Rayon infrastructure
|
||||
- Production-tested on multi-dimensional problems
|
||||
|
||||
## 📊 Benchmarks
|
||||
|
||||
### Differential Evolution (Rosenbrock 10D)
|
||||
- OptimizR (Rust): **0.12s**
|
||||
- SciPy (Python): **8.9s**
|
||||
- **Speedup: 74×**
|
||||
|
||||
### HMM Training (1000 observations, 3 states)
|
||||
- OptimizR (Rust): **0.03s**
|
||||
- hmmlearn (Python): **2.4s**
|
||||
- **Speedup: 80×**
|
||||
|
||||
### Mean Field Games (100×100 grid)
|
||||
- OptimizR (Rust): **0.4s**
|
||||
- Pure Python: **45s**
|
||||
- **Speedup: 112×**
|
||||
|
||||
## 🔧 Breaking Changes from v0.3.0
|
||||
|
||||
### Cargo Feature Flags
|
||||
```toml
|
||||
# OLD (v0.3.0):
|
||||
[features]
|
||||
default = ["python-bindings"] # Always included
|
||||
|
||||
# NEW (v1.0.0):
|
||||
[features]
|
||||
default = [] # No default features
|
||||
python-bindings = ["pyo3", "numpy"] # Opt-in
|
||||
```
|
||||
|
||||
**Impact:**
|
||||
- Rust-only users: No breaking changes (python-bindings not needed)
|
||||
- Python users: No impact (maturin automatically enables python-bindings)
|
||||
|
||||
If you're using OptimizR as a Rust library and explicitly depend on Python bindings:
|
||||
```toml
|
||||
# Update your Cargo.toml:
|
||||
[dependencies]
|
||||
optimizr = { version = "1.0", features = ["python-bindings"] }
|
||||
```
|
||||
|
||||
## 📝 Migration Guide
|
||||
|
||||
### From v0.3.0 to v1.0.0
|
||||
|
||||
**For Rust Users:**
|
||||
No code changes required. If you were using python-bindings explicitly, add it to features list.
|
||||
|
||||
**For Python Users:**
|
||||
```bash
|
||||
# Install via pip
|
||||
pip install optimiz-rs
|
||||
|
||||
# Or specify version
|
||||
pip install optimiz-rs==1.0.0
|
||||
```
|
||||
|
||||
**Note:** Package name changed from `optimizr` to `optimiz-rs` to avoid PyPI naming conflict.
|
||||
|
||||
**API Compatibility:**
|
||||
✅ All Python APIs remain unchanged
|
||||
✅ All Rust APIs remain unchanged
|
||||
✅ Function signatures are identical
|
||||
✅ Return types are identical
|
||||
✅ No deprecations or removals
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
- Fixed linking errors when using OptimizR as Rust-only library
|
||||
- Fixed PyInit__core symbol warning in maturin builds
|
||||
- Resolved flate2 yanked dependency warning
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### New Documentation
|
||||
- Complete ReadTheDocs site: https://optimiz-r.readthedocs.io
|
||||
- Getting Started guide
|
||||
- Installation instructions for all platforms
|
||||
- Tutorial notebooks (7 validated examples)
|
||||
- API reference with examples
|
||||
- Theory and mathematical background
|
||||
|
||||
### Validated Tutorial Notebooks
|
||||
1. ✅ **Hidden Markov Models** - Regime detection
|
||||
2. ✅ **MCMC Sampling** - Bayesian inference
|
||||
3. ✅ **Differential Evolution** - Global optimization
|
||||
4. ✅ **Optimal Control** - HJB solver (theory)
|
||||
5. ✅ **Real-World Applications** - Complete workflows
|
||||
6. ✅ **Performance Benchmarks** - Rust vs Python
|
||||
7. ✅ **Mean Field Games** - Population dynamics
|
||||
|
||||
## 🔮 Roadmap
|
||||
|
||||
### v1.1.0 (Q2 2026)
|
||||
- [ ] Additional DE variants (JADE, SHADE, L-SHADE)
|
||||
- [ ] Particle Swarm Optimization (PSO)
|
||||
- [ ] CMA-ES algorithm
|
||||
- [ ] More HMM emission distributions
|
||||
|
||||
### v1.2.0 (Q3 2026)
|
||||
- [ ] GPU acceleration via CUDA/ROCm
|
||||
- [ ] Additional language bindings (R, Julia, JavaScript)
|
||||
- [ ] Distributed computing support
|
||||
- [ ] Advanced parallel strategies
|
||||
|
||||
### v2.0.0 (2027)
|
||||
- [ ] Neural Evolution Strategies (NES)
|
||||
- [ ] Multi-objective optimization
|
||||
- [ ] Constraint handling methods
|
||||
- [ ] Advanced uncertainty quantification
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Built with:
|
||||
- [Rust](https://www.rust-lang.org/) - Systems programming language
|
||||
- [PyO3](https://pyo3.rs/) - Rust bindings for Python
|
||||
- [Maturin](https://www.maturin.rs/) - Build and publish Rust crates as Python packages
|
||||
- [NumPy](https://numpy.org/) - Numerical computing in Python
|
||||
|
||||
Inspired by:
|
||||
- scipy.optimize
|
||||
- scikit-learn
|
||||
- hmmlearn
|
||||
- emcee
|
||||
|
||||
## 📞 Support & Community
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/ThotDjehuty/optimiz-r/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/ThotDjehuty/optimiz-r/discussions)
|
||||
- **Email**: contact@hfthot-lab.eu
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
**OptimizR v1.0.0** - Fast optimization for data science and machine learning 🚀
|
||||
|
||||
Thank you to all contributors and early adopters who helped make this release possible!
|
||||
@@ -1,4 +1,4 @@
|
||||
# OptimizR Refactoring - Completion Report
|
||||
# Optimiz-rs Refactoring - Completion Report
|
||||
|
||||
## ✅ All Tasks Completed
|
||||
|
||||
@@ -263,7 +263,7 @@ pyproject.toml # ✅ Unchanged
|
||||
|
||||
✅ **All todo items completed successfully!**
|
||||
|
||||
The OptimizR codebase has been completely refactored with:
|
||||
The Optimiz-rs codebase has been completely refactored with:
|
||||
- ✅ Modular trait-based architecture
|
||||
- ✅ Functional programming patterns
|
||||
- ✅ Advanced design patterns (Strategy, Builder, Traits)
|
||||
@@ -1,4 +1,4 @@
|
||||
# OptimizR Development Guide
|
||||
# Optimiz-rs Development Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# OptimizR Enhancement Strategy
|
||||
# Optimiz-rs Enhancement Strategy
|
||||
|
||||
**Date**: January 2, 2025
|
||||
**Context**: Post-Polaroid Phase 4, exploring integration and improvements
|
||||
**Context**: Post-Polarway Phase 4, exploring integration and improvements
|
||||
**Based On**: v0.2.0 codebase review, roadmap analysis, synergy opportunities
|
||||
|
||||
## Current State Analysis
|
||||
@@ -58,19 +58,19 @@
|
||||
- Simulated Annealing
|
||||
- Ant Colony Optimization
|
||||
|
||||
## Synergy Opportunities: Polaroid + OptimizR
|
||||
## Synergy Opportunities: Polarway + Optimiz-rs
|
||||
|
||||
### 1. Time-Series Feature Engineering for HMM
|
||||
**Description**: Use Polaroid's time-series operations to create features for regime detection
|
||||
**Description**: Use Polarway's time-series operations to create features for regime detection
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Polaroid: Fast feature creation
|
||||
# Polarway: Fast feature creation
|
||||
df = client.lag(['price'], periods=1) # Lagged prices
|
||||
df = client.pct_change(['price'], periods=1) # Returns
|
||||
df = client.diff(['price'], periods=1) # Price changes
|
||||
|
||||
# OptimizR: Regime detection on features
|
||||
# Optimiz-rs: Regime detection on features
|
||||
returns = df['price_pct_change'].to_numpy()
|
||||
hmm = HMM(n_states=3) # Bull, Bear, Sideways
|
||||
hmm.fit(returns, n_iterations=100)
|
||||
@@ -78,8 +78,8 @@ states = hmm.predict(returns)
|
||||
```
|
||||
|
||||
**Value**:
|
||||
- Polaroid provides fast feature engineering (50-200× faster for large datasets)
|
||||
- OptimizR provides statistical inference (HMM regime detection)
|
||||
- Polarway provides fast feature engineering (50-200× faster for large datasets)
|
||||
- Optimiz-rs provides statistical inference (HMM regime detection)
|
||||
- Combined: Real-time regime switching for trading strategies
|
||||
|
||||
### 2. Risk Metrics on Time-Series Data
|
||||
@@ -87,18 +87,18 @@ states = hmm.predict(returns)
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Polaroid: Efficient return calculation
|
||||
# Polarway: Efficient return calculation
|
||||
df = client.pct_change(['price'], periods=1)
|
||||
returns = df['price_pct_change'].to_numpy()
|
||||
|
||||
# OptimizR: Risk analysis
|
||||
# Optimiz-rs: Risk analysis
|
||||
hurst = compute_hurst_exponent(returns) # Mean-reversion detection
|
||||
half_life = estimate_half_life(returns) # Reversion time
|
||||
risk_metrics = compute_risk_metrics(returns) # Comprehensive suite
|
||||
```
|
||||
|
||||
**Value**:
|
||||
- Fast preprocessing (Polaroid) + sophisticated analysis (OptimizR)
|
||||
- Fast preprocessing (Polarway) + sophisticated analysis (Optimiz-rs)
|
||||
- Useful for pairs trading, mean-reversion strategies
|
||||
- Real-time risk monitoring
|
||||
|
||||
@@ -107,11 +107,11 @@ risk_metrics = compute_risk_metrics(returns) # Comprehensive suite
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Polaroid: Multi-asset feature creation
|
||||
# Polarway: Multi-asset feature creation
|
||||
df = client.lag(['spy_price', 'vix'], periods=[1, 5, 20])
|
||||
df = client.pct_change(['spy_price'], periods=1)
|
||||
|
||||
# OptimizR: Solve optimal control problem
|
||||
# Optimiz-rs: Solve optimal control problem
|
||||
# State: [price, volatility regime]
|
||||
# Control: portfolio weights
|
||||
value_fn = solve_hjb_regime_switching(...)
|
||||
@@ -127,13 +127,13 @@ value_fn = solve_hjb_regime_switching(...)
|
||||
|
||||
**Implementation**:
|
||||
```python
|
||||
# Polaroid: Backtest execution (fast data ops)
|
||||
# Polarway: Backtest execution (fast data ops)
|
||||
def backtest_strategy(params):
|
||||
df = client.lag(['price'], periods=int(params[0]))
|
||||
# ... strategy logic ...
|
||||
return -sharpe_ratio # Minimize negative Sharpe
|
||||
|
||||
# OptimizR: Find optimal parameters
|
||||
# Optimiz-rs: Find optimal parameters
|
||||
result = differential_evolution(
|
||||
objective_fn=backtest_strategy,
|
||||
bounds=[(1, 50), (0.01, 0.5)], # [lag_period, threshold]
|
||||
@@ -143,8 +143,8 @@ result = differential_evolution(
|
||||
```
|
||||
|
||||
**Value**:
|
||||
- Polaroid handles heavy data processing
|
||||
- OptimizR finds optimal parameters
|
||||
- Polarway handles heavy data processing
|
||||
- Optimiz-rs finds optimal parameters
|
||||
- 74-88× faster than SciPy DE
|
||||
|
||||
## High-Priority Enhancements
|
||||
@@ -258,14 +258,14 @@ impl SHADEMemory {
|
||||
|
||||
### Priority 3: Time-Series Integration Helpers
|
||||
|
||||
**Problem**: Using Polaroid + OptimizR requires manual glue code
|
||||
**Problem**: Using Polarway + Optimiz-rs requires manual glue code
|
||||
|
||||
**Solution**: Create helper functions for common time-series + optimization patterns
|
||||
|
||||
**Implementation Strategy**:
|
||||
1. Add `timeseries_utils` module to OptimizR
|
||||
1. Add `timeseries_utils` module to Optimiz-rs
|
||||
2. Functions for common workflows
|
||||
3. Optional Polaroid integration (via feature flag)
|
||||
3. Optional Polarway integration (via feature flag)
|
||||
|
||||
**Code Outline**:
|
||||
```rust
|
||||
@@ -350,7 +350,7 @@ result = tsu.optimize_strategy_params(
|
||||
|
||||
1. **Session 1 (Current)**: Time-Series Integration Helpers (1-2 hours)
|
||||
- Low effort, immediate value
|
||||
- Makes Polaroid + OptimizR integration obvious
|
||||
- Makes Polarway + Optimiz-rs integration obvious
|
||||
- Creates examples for documentation
|
||||
|
||||
2. **Session 2**: Enable Rust-Native Parallelization (1-2 hours)
|
||||
@@ -372,7 +372,7 @@ result = tsu.optimize_strategy_params(
|
||||
For each enhancement:
|
||||
1. **Unit tests**: Algorithm correctness (sphere function, Rosenbrock)
|
||||
2. **Benchmarks**: Performance comparison (before/after)
|
||||
3. **Integration tests**: Polaroid + OptimizR workflows
|
||||
3. **Integration tests**: Polarway + Optimiz-rs workflows
|
||||
4. **Documentation**: Usage examples, API docs
|
||||
|
||||
## Git Commit Strategy (per MANDATORY rules)
|
||||
@@ -404,4 +404,4 @@ Each enhancement gets:
|
||||
|
||||
---
|
||||
|
||||
**Next Action**: Implement Priority 3 (Time-Series Integration Helpers) as it's lowest effort with immediate value for demonstrating Polaroid + OptimizR synergy.
|
||||
**Next Action**: Implement Priority 3 (Time-Series Integration Helpers) as it's lowest effort with immediate value for demonstrating Polarway + Optimiz-rs synergy.
|
||||
@@ -1,4 +1,4 @@
|
||||
# OptimizR Enhancement Suite - Implementation Complete
|
||||
# Optimiz-rs Enhancement Suite - Implementation Complete
|
||||
|
||||
**Date**: January 2, 2026
|
||||
**Session Duration**: ~3 hours
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Completed comprehensive enhancement suite for OptimizR v0.2.0, implementing all 3 priorities from the Enhancement Strategy:
|
||||
Completed comprehensive enhancement suite for Optimiz-rs v0.2.0, implementing all 3 priorities from the Enhancement Strategy:
|
||||
|
||||
1. ✅ **Time-Series Integration Helpers** (Priority 3)
|
||||
2. ✅ **Rust Parallelization** (Priority 2)
|
||||
3. ✅ **SHADE Algorithm** (Priority 1)
|
||||
|
||||
Additionally created integration examples combining Polaroid + OptimizR workflows.
|
||||
Additionally created integration examples combining Polarway + Optimiz-rs workflows.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,7 +22,7 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
|
||||
|
||||
### 1. Time-Series Integration Helpers (Commit: 9a8032e, 7f77f29)
|
||||
|
||||
**Purpose**: Bridge OptimizR's optimization with time-series analysis for financial workflows.
|
||||
**Purpose**: Bridge Optimiz-rs's optimization with time-series analysis for financial workflows.
|
||||
|
||||
**Implementation**:
|
||||
- Created `src/timeseries_utils.rs` (400+ lines)
|
||||
@@ -41,7 +41,7 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
|
||||
- All functions tested and working
|
||||
|
||||
**Impact**:
|
||||
- Enables Polaroid → OptimizR workflows
|
||||
- Enables Polarway → Optimiz-rs workflows
|
||||
- Simplifies regime detection with HMM
|
||||
- Streamlines pairs trading analysis
|
||||
|
||||
@@ -130,12 +130,12 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
|
||||
|
||||
### 4. Integration Examples (Included with parallelization)
|
||||
|
||||
**Purpose**: Demonstrate Polaroid + OptimizR workflows.
|
||||
**Purpose**: Demonstrate Polarway + Optimiz-rs workflows.
|
||||
|
||||
**Implementation**:
|
||||
- `examples/polaroid_optimizr_integration.py` (500+ lines)
|
||||
- `examples/polarway_optimizr_integration.py` (500+ lines)
|
||||
- 4 comprehensive workflows:
|
||||
1. **Regime Detection**: Polaroid features → HMM → regime classification
|
||||
1. **Regime Detection**: Polarway features → HMM → regime classification
|
||||
2. **Strategy Optimization**: Moving average crossover with DE
|
||||
3. **Risk Analysis**: Portfolio with rolling metrics
|
||||
4. **Pairs Trading**: Complete pipeline with cointegration check
|
||||
@@ -148,11 +148,11 @@ Additionally created integration examples combining Polaroid + OptimizR workflow
|
||||
|
||||
**Impact**:
|
||||
- End-to-end examples for financial analysis
|
||||
- Demonstrates Polaroid + OptimizR synergy
|
||||
- Demonstrates Polarway + Optimiz-rs synergy
|
||||
- Ready for production adaptation
|
||||
|
||||
**Files**:
|
||||
- `examples/polaroid_optimizr_integration.py`
|
||||
- `examples/polarway_optimizr_integration.py`
|
||||
- `examples/timeseries_integration.py`
|
||||
- `examples/parallel_de_benchmark.py`
|
||||
|
||||
@@ -221,9 +221,9 @@ All commits pushed to origin/main ✅
|
||||
|
||||
## 🎯 Alignment with Roadmap
|
||||
|
||||
All enhancements align with OptimizR v0.3.0 roadmap:
|
||||
All enhancements align with Optimiz-rs v0.3.0 roadmap:
|
||||
|
||||
- ✅ **Time-series integration**: Enable Polaroid workflows
|
||||
- ✅ **Time-series integration**: Enable Polarway workflows
|
||||
- ✅ **Parallelization**: Unlock Rayon infrastructure
|
||||
- ✅ **SHADE**: State-of-the-art adaptive DE
|
||||
|
||||
@@ -325,4 +325,4 @@ Future (v0.3.0+):
|
||||
|
||||
**Status**: ✅ **ALL OBJECTIVES COMPLETE**
|
||||
**Next**: Integrate SHADE into DE, performance testing
|
||||
**Version**: OptimizR v0.2.0 → v0.3.0 prep
|
||||
**Version**: Optimiz-rs v0.2.0 → v0.3.0 prep
|
||||
@@ -9,7 +9,7 @@ Example notebooks in `examples/notebooks/` **ARE WORKING CORRECTLY**! They use P
|
||||
- Automatic Rust backend when available
|
||||
- Graceful fallback to pure Python
|
||||
|
||||
## Actual OptimizR Python API (from lib.rs)
|
||||
## Actual Optimiz-rs Python API (from lib.rs)
|
||||
|
||||
### ✅ Available Functions/Classes:
|
||||
|
||||
@@ -155,7 +155,7 @@ from optimizr import (
|
||||
```
|
||||
|
||||
**Features Demonstrated:**
|
||||
- Direct comparison: OptimizR (Rust) vs Python libraries
|
||||
- Direct comparison: Optimiz-rs (Rust) vs Python libraries
|
||||
- Benchmarks against: hmmlearn, scipy, sklearn
|
||||
- Performance metrics and speedup calculations
|
||||
|
||||
@@ -178,7 +178,7 @@ from optimizr import (
|
||||
|
||||
### Python Wrapper Design (Brilliant!)
|
||||
|
||||
OptimizR uses a **two-layer architecture**:
|
||||
Optimiz-rs uses a **two-layer architecture**:
|
||||
|
||||
1. **Rust Core** (`src/` with PyO3):
|
||||
- `HMMParams` class
|
||||
@@ -237,7 +237,7 @@ This design is **excellent** because:
|
||||
|
||||
## Testing Summary
|
||||
|
||||
| Notebook | Status | OptimizR Features | Test Result |
|
||||
| Notebook | Status | Optimiz-rs Features | Test Result |
|
||||
|----------|--------|-------------------|-------------|
|
||||
| 01_hmm_tutorial.ipynb | ✅ PASS | HMM (Rust) | All cells run |
|
||||
| 02_mcmc_tutorial.ipynb | ✅ PASS | mcmc_sample | Imports OK |
|
||||
@@ -277,7 +277,7 @@ This design is **excellent** because:
|
||||
**Actual Status:** Notebooks use Python wrappers correctly
|
||||
|
||||
**What I Learned:**
|
||||
1. OptimizR has excellent two-layer design
|
||||
1. Optimiz-rs has excellent two-layer design
|
||||
2. Python wrappers provide familiar OOP interface
|
||||
3. Rust acceleration is transparent to users
|
||||
4. Only 1 minor fix needed (random_state parameter)
|
||||
@@ -1,8 +1,8 @@
|
||||
# OptimizR Project Summary
|
||||
# Optimiz-rs Project Summary
|
||||
|
||||
## What is OptimizR?
|
||||
## What is Optimiz-rs?
|
||||
|
||||
OptimizR is a **general-purpose optimization library** that provides high-performance implementations of advanced algorithms in Rust with easy-to-use Python bindings. It's designed to be fast, reliable, and production-ready for open-source distribution.
|
||||
Optimiz-rs is a **general-purpose optimization library** that provides high-performance implementations of advanced algorithms in Rust with easy-to-use Python bindings. It's designed to be fast, reliable, and production-ready for open-source distribution.
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -140,7 +140,7 @@ x_opt, f_min = differential_evolution(
|
||||
|
||||
## Differences from rust-hft-arbitrage-lab
|
||||
|
||||
| Aspect | rust-hft-arbitrage-lab | OptimizR |
|
||||
| Aspect | rust-hft-arbitrage-lab | Optimiz-rs |
|
||||
|--------|----------------------|----------|
|
||||
| **Purpose** | HFT trading strategies | General optimization library |
|
||||
| **Scope** | Trading-specific | Domain-agnostic |
|
||||
@@ -165,7 +165,7 @@ x_opt, f_min = differential_evolution(
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: OptimizR v0.1.0"
|
||||
git commit -m "Initial commit: Optimiz-rs v0.1.0"
|
||||
git remote add origin https://github.com/ThotDjehuty/optimiz-r.git
|
||||
git push -u origin main
|
||||
```
|
||||
@@ -217,7 +217,7 @@ Based on benchmarks from rust-hft-arbitrage-lab:
|
||||
## Marketing/Outreach
|
||||
|
||||
1. **Reddit**: r/rust, r/python, r/MachineLearning
|
||||
2. **Hacker News**: "Show HN: OptimizR - Fast optimization algorithms in Rust"
|
||||
2. **Hacker News**: "Show HN: Optimiz-rs - Fast optimization algorithms in Rust"
|
||||
3. **Twitter/X**: Tweet with #rustlang #python
|
||||
4. **PyPI**: Ensure good package description
|
||||
5. **GitHub Topics**: optimization, rust, python, scientific-computing
|
||||
@@ -1,8 +1,8 @@
|
||||
# OptimizR Refactoring Summary
|
||||
# Optimiz-rs Refactoring Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the major refactoring applied to OptimizR to improve modularity, introduce functional programming patterns, implement design patterns, and add concurrency support.
|
||||
This document summarizes the major refactoring applied to Optimiz-rs to improve modularity, introduce functional programming patterns, implement design patterns, and add concurrency support.
|
||||
|
||||
## Architecture Changes
|
||||
|
||||
@@ -369,7 +369,7 @@ impl ProposalStrategy for MyProposal {
|
||||
|
||||
## Conclusion
|
||||
|
||||
This refactoring significantly improves OptimizR's:
|
||||
This refactoring significantly improves Optimiz-rs's:
|
||||
- **Modularity**: Clear trait boundaries, easy to extend
|
||||
- **Maintainability**: Builder patterns, functional utilities reduce boilerplate
|
||||
- **Performance**: Parallel execution, memoization, lazy evaluation
|
||||
@@ -1,4 +1,4 @@
|
||||
# OptimizR Setup Complete! ✅
|
||||
# Optimiz-rs Setup Complete! ✅
|
||||
|
||||
## What Was Done
|
||||
|
||||
@@ -136,7 +136,7 @@ pytest tests/ -v -k HMM # Run HMM tests only
|
||||
|
||||
## Performance
|
||||
|
||||
OptimizR provides **50-100x speedup** over pure Python for:
|
||||
Optimiz-rs provides **50-100x speedup** over pure Python for:
|
||||
- HMM fitting (71x faster)
|
||||
- MCMC sampling (71x faster)
|
||||
- Differential Evolution (53x faster)
|
||||
@@ -144,7 +144,7 @@ OptimizR provides **50-100x speedup** over pure Python for:
|
||||
|
||||
## Summary
|
||||
|
||||
The OptimizR project is now **fully functional** with:
|
||||
The Optimiz-rs project is now **fully functional** with:
|
||||
- ✅ Zero compilation errors
|
||||
- ✅ All tests passing
|
||||
- ✅ Docker support
|
||||
@@ -1,7 +1,7 @@
|
||||
# Time-Series Integration Helpers Implementation Summary
|
||||
|
||||
## Overview
|
||||
Completed Priority 3 from Enhancement Strategy: Time-series integration helpers for OptimizR v0.3.0. These 6 helper functions bridge OptimizR's optimization capabilities with time-series analysis, particularly useful for regime-switching models and pairs trading strategies.
|
||||
Completed Priority 3 from Enhancement Strategy: Time-series integration helpers for Optimiz-rs v0.3.0. These 6 helper functions bridge Optimiz-rs's optimization capabilities with time-series analysis, particularly useful for regime-switching models and pairs trading strategies.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@@ -15,7 +15,7 @@ Completed Priority 3 from Enhancement Strategy: Time-series integration helpers
|
||||
- Volatility proxy: squared returns
|
||||
- Lagged returns for each lag period
|
||||
- Returns: Feature matrix (N-max_lag rows × (3 + num_lags) columns)
|
||||
- Use case: Prepare price data for OptimizR's HMM regime detection
|
||||
- Use case: Prepare price data for Optimiz-rs's HMM regime detection
|
||||
|
||||
2. **`rolling_hurst_exponent(returns: &[f64], window_size: usize) -> Vec<f64>`**
|
||||
- Purpose: Detect mean-reversion vs trending behavior
|
||||
@@ -146,7 +146,7 @@ import optimizr
|
||||
|
||||
prices = [100.0, 101.5, 99.8, 102.3, 103.7]
|
||||
features = optimizr.prepare_for_hmm_py(prices, [1, 2])
|
||||
# Use with OptimizR's HMM for regime detection
|
||||
# Use with Optimiz-rs's HMM for regime detection
|
||||
```
|
||||
|
||||
### Mean-Reversion Check
|
||||
@@ -0,0 +1,7 @@
|
||||
# Python dependencies for building documentation (pinned for RTD compatibility)
|
||||
sphinx>=7.0.0,<8.0.0
|
||||
furo==2023.9.10
|
||||
myst-parser==2.0.0
|
||||
sphinx-autodoc-typehints==1.24.0
|
||||
charset-normalizer>=3.4.0
|
||||
sphinxcontrib-mermaid>=0.9.2
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Replace the 4 remaining ASCII diagram blocks in mathematical_foundations.md
|
||||
with {figure} directives pointing to the new SVGs.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
MD = pathlib.Path(__file__).parent / "theory" / "mathematical_foundations.md"
|
||||
text = MD.read_text(encoding="utf-8")
|
||||
|
||||
# ── 1. HMM regime state machine → fig_hmm_regime ──────────────────────────
|
||||
old1 = '''\
|
||||
```
|
||||
HMM regime state machine (K = 3)
|
||||
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
|
||||
A₁₂ → A₂₃ →
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ State 1 │──────▶│ State 2 │──────▶│ State 3 │
|
||||
│ Bull │◀──────│ Neutral │◀──────│ Bear │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
← A₂₁ ← A₃₂
|
||||
|
||||
Emission B_k(y) = 𝒩(μ_k, σ_k²):
|
||||
┌────────┬────────┬────────┬──────────────────┐
|
||||
│ State │ μ │ σ │ Character │
|
||||
├────────┼────────┼────────┼──────────────────┤
|
||||
│ Bull │ +0.05 │ 0.12 │ high return, low vol │
|
||||
│ Neutral│ 0.00 │ 0.18 │ flat, medium vol │
|
||||
│ Bear │ -0.08 │ 0.35 │ crash, high vol │
|
||||
└────────┴────────┴────────┴──────────────────┘
|
||||
(self-transition: A₁₁=0.97, A₂₂=0.97, A₃₃=0.90)
|
||||
```'''
|
||||
|
||||
new1 = '''\
|
||||
```{figure} ../_static/diagrams/fig_hmm_regime.svg
|
||||
:align: center
|
||||
:width: 90%
|
||||
|
||||
HMM $K=3$ state machine with Bull / Neutral / Bear regimes and Gaussian emission
|
||||
parameters. Self-transitions $A_{11}=A_{22}=0.97$, $A_{33}=0.90$.
|
||||
```'''
|
||||
|
||||
# ── 2. Viterbi trellis → fig_viterbi_trellis ───────────────────────────────
|
||||
old2 = '''\
|
||||
```
|
||||
Viterbi trellis (K=3, T=4)
|
||||
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
|
||||
State t=1 t=2 t=3 t=4
|
||||
|
||||
1 ○─────────▶○─────────▶○─────────▶○
|
||||
╲ ╳
|
||||
2 ○─────────▶●─────────▶●─────────▶○ ● = MAP path
|
||||
╲ ╲ ╲
|
||||
3 ○─────────▶○─────────▶○─────────▶○
|
||||
|
||||
δ_t(k) = max_j [δ_{t−1}(j) · A_jk · B_k(y_t)]
|
||||
ψ_t(k) = argmax_j ← backtrack pointer
|
||||
|
||||
Traceback: z_4★ ← z_3★ ← z_2★ ← z_1★ via ψ
|
||||
```'''
|
||||
|
||||
new2 = r'''\
|
||||
```{figure} ../_static/diagrams/fig_viterbi_trellis.svg
|
||||
:align: center
|
||||
:width: 82%
|
||||
|
||||
Viterbi trellis ($K=3$, $T=4$). Filled nodes mark the MAP (most probable) state
|
||||
sequence; arrows show transition candidates. Backtracking via $\psi_t(k)$ recovers
|
||||
$z_1^\star \to z_4^\star$.
|
||||
```'''
|
||||
|
||||
# ── 3. Standard vs natural gradient (text comparison) → fig_std_vs_nat_gradient
|
||||
old3 = '''\
|
||||
```
|
||||
Standard vs natural gradient
|
||||
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
|
||||
Standard: θ_{k+1} = θ_k − η·∇ℒ Natural: θ_{k+1} = θ_k − η·ℐ(θ)^{−1}∇ℒ
|
||||
────────────────────────────────────────────
|
||||
|
||||
┌────────────────────┐ ┌────────────────────┐
|
||||
│ Flat ℝᵈ geometry │ │ Riemannian metric ℐ(θ) │
|
||||
│ Ignores curvature │ │ Adapts to geometry │
|
||||
│ Slow on ill-cond ℐ │ │ Reparam invariant │
|
||||
│ O(κ(ℐ)) iters │ │ O(1) on exp families │
|
||||
└────────────────────┘ └────────────────────┘
|
||||
|
||||
On Gaussian / exponential family: ℐ⁻¹∇ℒ = MLE step → 1 iteration!
|
||||
```'''
|
||||
|
||||
new3 = '''\
|
||||
```{figure} ../_static/diagrams/fig_std_vs_nat_gradient.svg
|
||||
:align: center
|
||||
:width: 88%
|
||||
|
||||
Standard versus natural gradient: geometric properties. On exponential families
|
||||
the natural gradient equals the MLE Newton step, achieving convergence in one
|
||||
iteration.
|
||||
```'''
|
||||
|
||||
# ── 4. Matrix Lie group hierarchy → fig_lie_group_hierarchy ─────────────────
|
||||
old4 = '''\
|
||||
```
|
||||
Matrix Lie group hierarchy
|
||||
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
|
||||
GL(n,ℝ) ─ all invertible n×n real matrices
|
||||
│
|
||||
├──▶ SL(n,ℝ) det = 1
|
||||
│
|
||||
├──▶ O(n) RᵀR = I (orthogonal)
|
||||
│ └─▶ SO(n) det = +1 (pure rotations)
|
||||
│ ↳ portfolio factor rotation, PCA constraints
|
||||
│
|
||||
└──▶ Sp(2n,ℝ) preserves symplectic form ω
|
||||
↳ Hamiltonian mechanics, PMP §4.2 / §10.4
|
||||
|
||||
H(n) Heisenberg ─ upper triangular, 1s on diagonal
|
||||
↳ path-signature feature maps
|
||||
```'''
|
||||
|
||||
new4 = r'''\
|
||||
```{figure} ../_static/diagrams/fig_lie_group_hierarchy.svg
|
||||
:align: center
|
||||
:width: 90%
|
||||
|
||||
Matrix Lie group hierarchy: subgroup inclusions and their quantitative-finance
|
||||
applications. $SO(n)$ underpins PCA factor rotation; $\mathrm{Sp}(2n,\mathbb{R})$
|
||||
governs Hamiltonian mechanics (PMP §10.4); $H(n)$ drives path-signature features.
|
||||
```'''
|
||||
|
||||
replacements = [(old1, new1), (old2, new2), (old3, new3), (old4, new4)]
|
||||
for i, (old, new) in enumerate(replacements, 1):
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
print(f" Block {i}: replaced OK")
|
||||
else:
|
||||
print(f" Block {i}: NOT FOUND — check encoding/whitespace")
|
||||
|
||||
MD.write_text(text, encoding="utf-8")
|
||||
print("Done.")
|
||||
@@ -0,0 +1,911 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate all matplotlib diagrams for mathematical_foundations.md.
|
||||
|
||||
Run from the docs/source directory (or workspace root):
|
||||
python docs/source/_gen_diagrams.py
|
||||
|
||||
Outputs SVG files to docs/source/_static/diagrams/
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
import matplotlib.ticker as mticker
|
||||
from scipy.stats import norm
|
||||
|
||||
# ─── output dir ─────────────────────────────────────────────────────────────
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_static", "diagrams")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
# ─── palette & defaults ─────────────────────────────────────────────────────
|
||||
C0 = "#2E6BE5" # blue
|
||||
C1 = "#E8850A" # orange
|
||||
C2 = "#27AE60" # green
|
||||
C3 = "#D62728" # red
|
||||
GRAY = "#888888"
|
||||
BAND = "#AACBE8"
|
||||
|
||||
matplotlib.rcParams.update({
|
||||
"font.size" : 11,
|
||||
"axes.titlesize" : 12,
|
||||
"axes.labelsize" : 11,
|
||||
"xtick.labelsize" : 9,
|
||||
"ytick.labelsize" : 9,
|
||||
"axes.spines.top" : False,
|
||||
"axes.spines.right" : False,
|
||||
"figure.dpi" : 150,
|
||||
"savefig.bbox" : "tight",
|
||||
"savefig.transparent" : False,
|
||||
"figure.facecolor" : "white",
|
||||
"axes.facecolor" : "white",
|
||||
"lines.linewidth" : 1.8,
|
||||
"text.usetex" : False,
|
||||
})
|
||||
|
||||
def save(name):
|
||||
plt.savefig(os.path.join(OUT, name + ".svg"))
|
||||
plt.close()
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §1 DIFFERENTIAL EVOLUTION
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_de_mutation():
|
||||
r1 = np.array([0.5, 0.3])
|
||||
r2 = np.array([1.2, 1.4])
|
||||
r3 = np.array([1.8, 0.6])
|
||||
F = 0.7
|
||||
vi = r1 + F * (r2 - r3)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 4.2))
|
||||
|
||||
# difference vector r3 → r2
|
||||
ax.annotate("", r2, r3,
|
||||
arrowprops=dict(arrowstyle="-|>", color=C2, lw=2.0, mutation_scale=14))
|
||||
mid = (r2 + r3) / 2
|
||||
ax.text(mid[0] - 0.05, mid[1] + 0.09,
|
||||
r"$F(\mathbf{x}_{r_2}-\mathbf{x}_{r_3})$",
|
||||
ha="center", fontsize=10, color=C2)
|
||||
|
||||
# mutation arrow r1 → vi (dashed)
|
||||
ax.annotate("", vi, r1,
|
||||
arrowprops=dict(arrowstyle="-|>", color=C1, lw=2.0,
|
||||
mutation_scale=14, linestyle="dashed"))
|
||||
ax.text((r1[0]+vi[0])/2, (r1[1]+vi[1])/2 - 0.1,
|
||||
r"$+F(\cdots)$", ha="center", fontsize=9, color=C1)
|
||||
|
||||
pts = {
|
||||
r"$\mathbf{x}_{r_1}$ (base)": (r1, C0),
|
||||
r"$\mathbf{x}_{r_2}$": (r2, C0),
|
||||
r"$\mathbf{x}_{r_3}$": (r3, C0),
|
||||
r"$\mathbf{v}_i$ (mutant)": (vi, C1),
|
||||
}
|
||||
for lbl, (p, col) in pts.items():
|
||||
ax.scatter(*p, s=90, color=col, zorder=6)
|
||||
offset = (0.05, 0.07)
|
||||
if "mutant" in lbl:
|
||||
offset = (0.07, 0.05)
|
||||
ax.text(p[0] + offset[0], p[1] + offset[1], lbl, fontsize=10, color=col)
|
||||
|
||||
ax.set_xlim(0.1, 2.5); ax.set_ylim(0.0, 1.85)
|
||||
ax.set_xlabel(r"$x_1$"); ax.set_ylabel(r"$x_2$")
|
||||
ax.set_title(r"DE Mutation: $\mathbf{v}_i = \mathbf{x}_{r_1} + F\,(\mathbf{x}_{r_2} - \mathbf{x}_{r_3})$")
|
||||
ax.set_aspect("equal", adjustable="box")
|
||||
save("fig_de_mutation")
|
||||
|
||||
|
||||
def fig_rastrigin():
|
||||
x = np.linspace(-2.5, 2.5, 800)
|
||||
y = 10 + x**2 - 10 * np.cos(2 * np.pi * x)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(x, y, color=C0, lw=2, label=r"$f(x) = 10 + x^2 - 10\cos(2\pi x)$")
|
||||
ax.fill_between(x, y, alpha=0.07, color=C0)
|
||||
ax.axhline(0, color=GRAY, lw=0.7, ls=":")
|
||||
|
||||
# global minimum
|
||||
ax.scatter([0], [0], s=110, color=C1, zorder=6, label=r"global min $f^*=0$", marker="*")
|
||||
|
||||
# local minima
|
||||
lm_x = np.array([-2.0, -1.0, 1.0, 2.0])
|
||||
lm_y = 10 + lm_x**2 - 10 * np.cos(2 * np.pi * lm_x)
|
||||
ax.scatter(lm_x, lm_y, s=55, color=C3, zorder=5, label="local minima", marker="o")
|
||||
|
||||
ax.annotate(r"$\approx 10^d$ local pits", (1.0, lm_y[2]),
|
||||
(1.5, 12), fontsize=9, color=C3,
|
||||
arrowprops=dict(arrowstyle="->", color=C3, lw=1.0))
|
||||
|
||||
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$f(x)$")
|
||||
ax.set_title(r"Rastrigin function ($d = 1$) — many local minima")
|
||||
ax.legend(fontsize=9, framealpha=0.6)
|
||||
save("fig_rastrigin")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.1 BROWNIAN MOTION
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_random_walk():
|
||||
rng = np.random.default_rng(42)
|
||||
n = 300
|
||||
t = np.linspace(0, 1, n)
|
||||
W = np.cumsum(rng.choice([-1, 1], size=n)) / np.sqrt(n)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.5))
|
||||
ax.plot(t, W, color=C0, lw=1.4)
|
||||
ax.axhline(0, color=GRAY, lw=0.8, ls="--", alpha=0.6)
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$W_t^{(n)}$")
|
||||
ax.set_title(r"Coin-flip random walk ($n=300$) $\longrightarrow$ Brownian motion as $n\to\infty$")
|
||||
save("fig_random_walk")
|
||||
|
||||
|
||||
def fig_bm_fan():
|
||||
rng = np.random.default_rng(0)
|
||||
n, dt = 500, 0.002
|
||||
npaths = 10
|
||||
ts = np.linspace(0, 1, n)
|
||||
paths = np.cumsum(rng.normal(0, np.sqrt(dt), (npaths, n)), axis=1)
|
||||
paths[:, 0] = 0
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 4.2))
|
||||
lo, hi = -2 * np.sqrt(ts), 2 * np.sqrt(ts)
|
||||
ax.fill_between(ts, lo, hi, alpha=0.13, color=C0, label=r"$\pm 2\sqrt{t}$ (95% band)")
|
||||
ax.plot(ts, hi, color=C0, lw=1.2, ls="--", alpha=0.55)
|
||||
ax.plot(ts, lo, color=C0, lw=1.2, ls="--", alpha=0.55)
|
||||
colors_cycle = plt.colormaps["tab10"](np.linspace(0, 0.9, npaths))
|
||||
for i, p in enumerate(paths):
|
||||
ax.plot(ts, p, lw=0.9, alpha=0.75, color=colors_cycle[i])
|
||||
ax.axhline(0, color=GRAY, lw=0.8, ls=":")
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$W_t$")
|
||||
ax.set_title(r"Brownian motion — sample paths spread as $\sqrt{t}$ (trumpet fan)")
|
||||
ax.legend(fontsize=9, framealpha=0.7)
|
||||
save("fig_bm_fan")
|
||||
|
||||
|
||||
def fig_gbm():
|
||||
rng = np.random.default_rng(7)
|
||||
T, n, dt = 1.0, 500, 0.002
|
||||
mu, sigma, S0 = 0.10, 0.30, 1.0
|
||||
ts = np.linspace(0, T, n)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(ts, S0 * np.exp(mu * ts), color=C1, lw=1.8, ls="--",
|
||||
label=r"$\mathbb{E}[S_t] = S_0 e^{\mu t}$")
|
||||
ax.plot(ts, S0 * np.exp((mu - 0.5*sigma**2) * ts), color=C2, lw=1.5, ls=":",
|
||||
label=r"median $\approx S_0 e^{(\mu-\sigma^2/2)t}$")
|
||||
colors_cycle = plt.colormaps["Blues"](np.linspace(0.4, 0.85, 7))
|
||||
for i in range(7):
|
||||
W = np.cumsum(rng.normal(0, np.sqrt(dt), n))
|
||||
S = S0 * np.exp((mu - 0.5*sigma**2) * ts + sigma * W)
|
||||
ax.plot(ts, S, lw=0.9, alpha=0.7, color=colors_cycle[i])
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$S_t$")
|
||||
ax.set_title(r"Geometric Brownian motion ($\mu=0.10,\;\sigma=0.30$)")
|
||||
ax.legend(fontsize=9, framealpha=0.6)
|
||||
save("fig_gbm")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.2 ITŌ CALCULUS
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_ito_correction():
|
||||
t = np.linspace(0, 2.2, 300)
|
||||
mu, sigma = 0.12, 0.30
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(t, mu * t, color=C1, lw=2, ls="--",
|
||||
label=r"Naïve slope $\mu t$ (wrong)")
|
||||
ax.plot(t, (mu - 0.5*sigma**2) * t, color=C0, lw=2,
|
||||
label=r"Itō slope $(\mu - \sigma^2/2)\,t$ (correct)")
|
||||
|
||||
# gap annotation at t = 1.8
|
||||
g_x = 1.8
|
||||
y_top = mu * g_x
|
||||
y_bot = (mu - 0.5*sigma**2) * g_x
|
||||
ax.annotate("", (g_x, y_bot), (g_x, y_top),
|
||||
arrowprops=dict(arrowstyle="<->", color=C3, lw=1.6))
|
||||
ax.text(g_x + 0.07, (y_top + y_bot) / 2,
|
||||
r"gap $= \sigma^2 T/2$", fontsize=9, color=C3, va="center")
|
||||
|
||||
ax.axhline(0, color=GRAY, lw=0.6, ls=":")
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$\mathbb{E}[\log S_t] - \log S_0$")
|
||||
ax.set_title(r"Itō correction: $\mathbb{E}[\log S_t]$ always below the naïve slope $\mu t$")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_ito_correction")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.3 FOKKER-PLANCK
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_fokker_planck():
|
||||
x = np.linspace(-0.5, 5.5, 600)
|
||||
mu_drift, sigma_diff = 0.8, 0.3
|
||||
times = [0.05, 0.5, 1.5]
|
||||
colors = [C3, C2, C0]
|
||||
labels = [r"$t = 0.05$ (narrow spike)",
|
||||
r"$t = 0.50$",
|
||||
r"$t = 1.50$ (wide, drifted)"]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
for t, col, lbl in zip(times, colors, labels):
|
||||
mean = mu_drift * t
|
||||
std = sigma_diff * np.sqrt(t)
|
||||
y = norm.pdf(x, mean, std)
|
||||
ax.plot(x, y, color=col, lw=2, label=lbl)
|
||||
ax.fill_between(x, y, alpha=0.10, color=col)
|
||||
|
||||
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$p(t, x)$")
|
||||
ax.set_title(r"Fokker-Planck: density drifts $(\mu=0.8)$ and broadens $(\sigma=0.3)$")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_fokker_planck")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.3 EULER-MARUYAMA vs MILSTEIN
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_em_milstein():
|
||||
dts = np.array([0.1, 0.05, 0.02, 0.01, 0.005, 0.001])
|
||||
em_err = 0.38 * dts**0.5
|
||||
mil_err = 0.19 * dts**1.0
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 4))
|
||||
ax.loglog(dts, em_err, "o-", color=C0, lw=2, ms=7,
|
||||
label=r"Euler-Maruyama (order $1/2$)")
|
||||
ax.loglog(dts, mil_err, "s--", color=C1, lw=2, ms=7,
|
||||
label=r"Milstein (order $1$)")
|
||||
ax.set_xlabel(r"Step size $\Delta t$")
|
||||
ax.set_ylabel(r"Strong error $\|X_T - \hat{X}_T\|$")
|
||||
ax.set_title("SDE numerical schemes — strong convergence order")
|
||||
ax.legend(fontsize=10); ax.grid(True, which="both", alpha=0.3)
|
||||
save("fig_em_milstein")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.4 ORNSTEIN-UHLENBECK
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_ou_path():
|
||||
rng = np.random.default_rng(3)
|
||||
T, n, dt = 5.0, 2000, 0.0025
|
||||
kappa, theta, sigma = 3.0, 0.5, 0.4
|
||||
X = np.zeros(n); X[0] = 2.0
|
||||
for i in range(1, n):
|
||||
X[i] = X[i-1] + kappa * (theta - X[i-1]) * dt + sigma * rng.normal(0, np.sqrt(dt))
|
||||
|
||||
ts = np.linspace(0, T, n)
|
||||
sig_inf = sigma / np.sqrt(2 * kappa)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(ts, X, color=C0, lw=1.0, alpha=0.9, label=r"$X_t$")
|
||||
ax.axhline(theta, color=C1, lw=1.8, ls="--",
|
||||
label=fr"$\theta = {theta}$ (long-run mean)")
|
||||
ax.fill_between(ts,
|
||||
theta - 2 * sig_inf,
|
||||
theta + 2 * sig_inf,
|
||||
alpha=0.10, color=GRAY, label=r"$\theta \pm 2\sigma_\infty$")
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$X_t$")
|
||||
ax.set_title(fr"Ornstein-Uhlenbeck ($\kappa={kappa},\;\theta={theta},\;\sigma={sigma}$) — mean-reversion")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_ou_path")
|
||||
|
||||
|
||||
def fig_ou_transition():
|
||||
x = np.linspace(-0.3, 2.6, 500)
|
||||
kappa, theta, sigma, x0 = 3.0, 0.5, 0.4, 2.0
|
||||
taus = [0.1, 0.5, 2.0]
|
||||
colors = [C3, C2, C0]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
for tau, col in zip(taus, colors):
|
||||
mean = theta + (x0 - theta) * np.exp(-kappa * tau)
|
||||
var = sigma**2 / (2 * kappa) * (1 - np.exp(-2 * kappa * tau))
|
||||
y = norm.pdf(x, mean, np.sqrt(var))
|
||||
ax.plot(x, y, color=col, lw=2,
|
||||
label=fr"$\tau = {tau:.1f}$ (mean $= {mean:.2f}$)")
|
||||
ax.fill_between(x, y, alpha=0.09, color=col)
|
||||
ax.axvline(theta, color=C1, lw=1.3, ls="--", label=fr"$\theta = {theta}$")
|
||||
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$p(x_\tau \mid x_0)$")
|
||||
ax.set_title(r"OU transition density: drifts toward $\theta$, widens over time")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_ou_transition")
|
||||
|
||||
|
||||
def fig_ou_loglik():
|
||||
kappa_v = np.linspace(10, 120, 80)
|
||||
theta_v = np.linspace(-0.005, 0.011, 80)
|
||||
K, T = np.meshgrid(kappa_v, theta_v)
|
||||
Z = -(((K - 55) / 22)**2 + ((T - 0.003) / 0.003)**2)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.2, 4.5))
|
||||
cf = ax.contourf(theta_v * 1000, kappa_v, Z.T, levels=20, cmap="Blues")
|
||||
ax.contour(theta_v * 1000, kappa_v, Z.T, levels=8,
|
||||
colors="white", linewidths=0.7, alpha=0.55)
|
||||
ax.plot(3, 55, "*", color=C1, ms=16, zorder=5,
|
||||
label=r"MLE $\hat\theta, \hat\kappa$")
|
||||
plt.colorbar(cf, ax=ax, label="Log-likelihood (normalised)")
|
||||
ax.set_xlabel(r"$\theta \times 10^3$"); ax.set_ylabel(r"$\kappa$")
|
||||
ax.set_title(r"OU log-likelihood surface $\ell(\kappa, \theta \mid \hat\sigma)$")
|
||||
ax.legend(fontsize=10)
|
||||
save("fig_ou_loglik")
|
||||
|
||||
|
||||
def fig_ou_residuals():
|
||||
rng = np.random.default_rng(9)
|
||||
r = rng.normal(0, 1, 600)
|
||||
x = np.linspace(-4, 4, 300)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 3.8))
|
||||
ax.hist(r, bins=32, density=True, color=C0, alpha=0.50,
|
||||
label="Standardised residuals")
|
||||
ax.plot(x, norm.pdf(x), color=C1, lw=2.2,
|
||||
label=r"$\mathcal{N}(0,1)$ theory")
|
||||
ax.set_xlabel(r"$r_i$"); ax.set_ylabel("Density")
|
||||
ax.set_title(r"OU residual diagnostic: $r_i = (X_{t_i} - \hat\mu_i)/\hat\sigma$")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_ou_residuals")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §3 JUMP PROCESSES
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_poisson():
|
||||
rng = np.random.default_rng(1)
|
||||
lam, T = 2, 4.0
|
||||
arrivals, t = [], 0.0
|
||||
while True:
|
||||
t += rng.exponential(1 / lam)
|
||||
if t > T: break
|
||||
arrivals.append(t)
|
||||
|
||||
ts = np.concatenate([[0.0], arrivals, [T]])
|
||||
ns = np.arange(len(ts) - 1)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.5))
|
||||
for i, (t0, t1, n) in enumerate(zip(ts[:-1], ts[1:], ns)):
|
||||
ax.hlines(n, t0, t1, color=C0, lw=2.8)
|
||||
if i < len(arrivals):
|
||||
ax.vlines(t1, n, n + 1, color=C0, lw=2.0, linestyle=":")
|
||||
ax.scatter([t1], [n], s=45, color="white", edgecolors=C0, zorder=5, lw=1.5)
|
||||
ax.scatter([t1], [n + 1], s=45, color=C0, zorder=5)
|
||||
|
||||
ax.yaxis.set_major_locator(mticker.MaxNLocator(integer=True))
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$N_t$")
|
||||
ax.set_title(fr"Poisson process ($\lambda = {lam}$ jumps/unit) — inter-arrivals $\sim \mathrm{{Exp}}(\lambda)$")
|
||||
save("fig_poisson")
|
||||
|
||||
|
||||
def fig_jump_diffusion():
|
||||
rng = np.random.default_rng(11)
|
||||
T, n, dt = 1.0, 1000, 0.001
|
||||
mu, sigma, lam = 0.05, 0.18, 2.5
|
||||
ts = np.linspace(0, T, n)
|
||||
S = np.ones(n)
|
||||
jump_times = np.sort(rng.uniform(0, T, rng.poisson(lam * T)))
|
||||
|
||||
for i in range(1, n):
|
||||
dW = rng.normal(0, np.sqrt(dt))
|
||||
S[i] = S[i-1] * np.exp((mu - 0.5 * sigma**2) * dt + sigma * dW)
|
||||
if np.any((ts[i-1] < jump_times) & (jump_times <= ts[i])):
|
||||
S[i] *= np.exp(rng.normal(0.0, 0.09))
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(ts, S, color=C0, lw=1.3, label=r"$S_t$ (jump-diffusion path)")
|
||||
# mark jump locations
|
||||
jt_idx = [np.searchsorted(ts, jt) for jt in jump_times if jt < T]
|
||||
ax.scatter(ts[jt_idx], S[jt_idx], s=50, color=C3, zorder=5,
|
||||
label=r"Poisson jump $\tau_k$", marker="v")
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$S_t$")
|
||||
ax.set_title(r"Merton jump-diffusion ($\lambda = 2.5$/yr, $\sigma_J = 9\%$)")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_jump_diffusion")
|
||||
|
||||
|
||||
def fig_levy_tails():
|
||||
x = np.linspace(0.05, 5, 600)
|
||||
gauss_tail = norm.pdf(x)
|
||||
gauss_tail /= gauss_tail[0]
|
||||
vg_tail = np.exp(-1.5 * x) / x
|
||||
vg_tail /= vg_tail[0]
|
||||
alpha_tail = x ** (-1.8)
|
||||
alpha_tail /= alpha_tail[0]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.5, 4))
|
||||
ax.semilogy(x, gauss_tail, lw=2, color=C0,
|
||||
label=r"Gaussian ($\nu \equiv 0$)")
|
||||
ax.semilogy(x, vg_tail, lw=2, color=C2,
|
||||
label=r"Variance Gamma ($\nu \propto e^{-c|z|}/|z|$)")
|
||||
ax.semilogy(x, alpha_tail, lw=2, color=C1, ls="--",
|
||||
label=r"$\alpha$-stable ($\nu \propto |z|^{-1-\alpha}$, heaviest)")
|
||||
ax.set_xlabel(r"Jump size $|z|$")
|
||||
ax.set_ylabel(r"Lévy density $\nu(dz)/dz$ (log scale)")
|
||||
ax.set_title("Lévy measure tails — heavier tail = more frequent/larger jumps")
|
||||
ax.legend(fontsize=9); ax.grid(True, which="both", alpha=0.25)
|
||||
save("fig_levy_tails")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §6 KALMAN FILTER
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_kalman_covariance():
|
||||
t = np.linspace(0, 30, 300)
|
||||
Pinf = 0.17
|
||||
Pt = Pinf + (1.0 - Pinf) * np.exp(-0.35 * t)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.5))
|
||||
ax.plot(t, Pt, color=C0, lw=2, label=r"$P_t$ (error covariance)")
|
||||
ax.axhline(Pinf, color=C1, lw=1.6, ls="--",
|
||||
label=fr"$P_\infty \approx {Pinf}$ (steady-state)")
|
||||
ax.fill_between(t, Pt, Pinf, alpha=0.10, color=C0)
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$P_t$")
|
||||
ax.set_title(r"Kalman filter: error covariance converges exponentially to $P_\infty$")
|
||||
ax.legend(fontsize=9); ax.set_ylim(0, 1.05)
|
||||
save("fig_kalman_covariance")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §7 MCMC
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_mcmc_energy():
|
||||
x = np.linspace(-5, 5, 600)
|
||||
pi = 0.5 * norm.pdf(x, -1.5, 0.8) + 0.5 * norm.pdf(x, 1.5, 0.9)
|
||||
U = -np.log(pi + 1e-12)
|
||||
U -= U.min()
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(x, U, color=C0, lw=2)
|
||||
ax.fill_between(x, U, alpha=0.08, color=C0)
|
||||
ax.scatter([-1.5, 1.5], [U[np.abs(x + 1.5).argmin()],
|
||||
U[np.abs(x - 1.5).argmin()]],
|
||||
s=90, color=C2, zorder=5, label=r"modes of $\pi$")
|
||||
saddle_i = np.abs(x).argmin()
|
||||
ax.scatter([x[saddle_i]], [U[saddle_i]], s=90, color=C3,
|
||||
zorder=5, marker="^", label="energy barrier")
|
||||
ax.annotate(r"accept with $e^{-\Delta U}$",
|
||||
(x[saddle_i] + 0.3, U[saddle_i] - 0.4),
|
||||
(2.2, 1.2), fontsize=9, color=C3,
|
||||
arrowprops=dict(arrowstyle="->", color=C3, lw=1.0))
|
||||
ax.set_xlabel(r"$x$"); ax.set_ylabel(r"$U(x) = -\log\pi(x)$")
|
||||
ax.set_title(r"MCMC energy landscape (bimodal target $\pi$)")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_mcmc_energy")
|
||||
|
||||
|
||||
def fig_mcmc_trace():
|
||||
rng = np.random.default_rng(42)
|
||||
x_cur = -1.5
|
||||
chain = [x_cur]
|
||||
for _ in range(2999):
|
||||
prop = x_cur + rng.normal(0, 0.8)
|
||||
pi_cur = 0.5 * norm.pdf(x_cur, -1.5, 0.8) + 0.5 * norm.pdf(x_cur, 1.5, 0.9)
|
||||
pi_prop = 0.5 * norm.pdf(prop, -1.5, 0.8) + 0.5 * norm.pdf(prop, 1.5, 0.9)
|
||||
x_cur = prop if rng.random() < pi_prop / pi_cur else x_cur
|
||||
chain.append(x_cur)
|
||||
chain = np.array(chain)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
|
||||
axes[0].plot(chain, lw=0.6, color=C0, alpha=0.8)
|
||||
axes[0].axhline(0, color=GRAY, lw=0.7, ls=":")
|
||||
axes[0].set_xlabel("Iteration"); axes[0].set_ylabel(r"$x_t$")
|
||||
axes[0].set_title("Trace plot — chain mixes between both modes")
|
||||
|
||||
x = np.linspace(-5, 5, 400)
|
||||
true_pi = 0.5 * norm.pdf(x, -1.5, 0.8) + 0.5 * norm.pdf(x, 1.5, 0.9)
|
||||
axes[1].hist(chain, bins=50, density=True, color=C0, alpha=0.50,
|
||||
label="MCMC samples")
|
||||
axes[1].plot(x, true_pi, color=C1, lw=2.2, label=r"true $\pi(x)$")
|
||||
axes[1].set_xlabel(r"$x$"); axes[1].set_ylabel("Density")
|
||||
axes[1].set_title("Marginal distribution")
|
||||
axes[1].legend(fontsize=9)
|
||||
plt.tight_layout()
|
||||
save("fig_mcmc_trace")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §9 INFORMATION THEORY
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_kl_asymmetry():
|
||||
x = np.linspace(-10, 10, 800)
|
||||
p = norm.pdf(x, 0, 1)
|
||||
q = norm.pdf(x, 0, 4)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(x, p, color=C0, lw=2, label=r"$p = \mathcal{N}(0,1)$ (narrow)")
|
||||
ax.plot(x, q, color=C1, lw=2, ls="--", label=r"$q = \mathcal{N}(0,4)$ (wide)")
|
||||
ax.fill_between(x, p, alpha=0.12, color=C0)
|
||||
ax.fill_between(x, q, alpha=0.08, color=C1)
|
||||
|
||||
dx = x[1] - x[0]
|
||||
eps = 1e-12
|
||||
kl_pq = float(np.sum(p * np.log((p + eps) / (q + eps))) * dx)
|
||||
kl_qp = float(np.sum(q * np.log((q + eps) / (p + eps)) * dx))
|
||||
ax.text(-9.5, 0.085,
|
||||
fr"$D_{{KL}}(p\|q) \approx {kl_pq:.2f}$ (small: $q$ covers $p$)",
|
||||
fontsize=9, color=C0)
|
||||
ax.text(-9.5, 0.066,
|
||||
fr"$D_{{KL}}(q\|p) \approx {kl_qp:.2f}$ (large: $p$ misses tails of $q$)",
|
||||
fontsize=9, color=C1)
|
||||
ax.set_xlabel(r"$x$"); ax.set_ylabel("Density")
|
||||
ax.set_title(r"KL divergence asymmetry: $D_{KL}(p\|q) \neq D_{KL}(q\|p)$")
|
||||
ax.legend(fontsize=9)
|
||||
save("fig_kl_asymmetry")
|
||||
|
||||
|
||||
def fig_fisher_curvature():
|
||||
theta = np.linspace(-3, 3, 400)
|
||||
sigma_vals = [0.5, 1.0, 2.0]
|
||||
colors = [C0, C2, C1]
|
||||
labels = [r"$\sigma=0.5$ (high $\mathcal{I}$, sharp peak)",
|
||||
r"$\sigma=1.0$",
|
||||
r"$\sigma=2.0$ (low $\mathcal{I}$, flat peak)"]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
for s, col, lbl in zip(sigma_vals, colors, labels):
|
||||
logL = -0.5 * (theta / s)**2 - np.log(s)
|
||||
logL -= logL.max()
|
||||
ax.plot(theta, logL, lw=2, color=col, label=lbl)
|
||||
|
||||
ax.axvline(0, color=GRAY, lw=0.8, ls=":")
|
||||
ax.set_xlabel(r"$\theta$"); ax.set_ylabel(r"$\log\mathcal{L}(\theta \mid x_\mathrm{obs})$ (centred)")
|
||||
ax.set_title(r"Fisher information = log-likelihood curvature at $\theta^*$")
|
||||
ax.legend(fontsize=9); ax.set_ylim(-4.2, 0.3)
|
||||
save("fig_fisher_curvature")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §10 DIFFERENTIAL GEOMETRY
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_curvatures():
|
||||
fig, axes = plt.subplots(1, 3, figsize=(10, 3.5))
|
||||
|
||||
# K > 0 — converging geodesics
|
||||
ax = axes[0]
|
||||
ax.set_aspect("equal"); ax.axis("off")
|
||||
theta_arc = np.linspace(0, np.pi, 200)
|
||||
ax.plot(np.cos(theta_arc), np.sin(theta_arc), color=GRAY, lw=1.5, ls="--", alpha=0.35)
|
||||
for ang in np.linspace(-0.45, 0.45, 7):
|
||||
r = np.linspace(0, 1, 60)
|
||||
ax.plot(r * np.sin(ang), r * np.cos(ang), color=C0, lw=1.5, alpha=0.75)
|
||||
ax.scatter([0], [0], s=70, color=C1, zorder=5)
|
||||
ax.text(0, -0.12, "meet at N pole", ha="center", fontsize=8, color=GRAY)
|
||||
ax.set_title(r"$K > 0$ (sphere $S^2$)" + "\ngeodesics converge", fontsize=10)
|
||||
|
||||
# K = 0 — parallel
|
||||
ax = axes[1]; ax.axis("off")
|
||||
for y in np.linspace(-0.8, 0.8, 7):
|
||||
ax.plot([-1, 1], [y, y], color=C0, lw=1.5)
|
||||
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.2, 1.2)
|
||||
ax.text(0, -1.1, "remain equidistant", ha="center", fontsize=8, color=GRAY)
|
||||
ax.set_title(r"$K = 0$ (flat $\mathbb{R}^2$)" + "\nparallel geodesics", fontsize=10)
|
||||
|
||||
# K < 0 — diverging
|
||||
ax = axes[2]; ax.axis("off")
|
||||
for ang in np.linspace(-0.55, 0.55, 7):
|
||||
r = np.linspace(0, 1.2, 60)
|
||||
scale = 1 + 0.55 * r
|
||||
ax.plot(r * np.sin(ang * scale), r * np.cos(ang * scale), color=C0, lw=1.5, alpha=0.75)
|
||||
ax.scatter([0], [0], s=70, color=C1, zorder=5)
|
||||
ax.set_xlim(-1.1, 1.1); ax.set_ylim(-0.15, 1.5)
|
||||
ax.text(0, -0.12, "spread exponentially", ha="center", fontsize=8, color=GRAY)
|
||||
ax.set_title(r"$K < 0$ (hyperbolic $H^2$)" + "\ngeodesics diverge", fontsize=10)
|
||||
|
||||
plt.suptitle("Sectional curvature determines geodesic behaviour", y=1.03, fontsize=12)
|
||||
plt.tight_layout()
|
||||
save("fig_curvatures")
|
||||
|
||||
|
||||
def fig_natural_gradient():
|
||||
fig, axes = plt.subplots(1, 2, figsize=(9, 3.8))
|
||||
theta1 = np.linspace(-2, 2, 300)
|
||||
theta2 = np.linspace(-2, 2, 300)
|
||||
T1, T2 = np.meshgrid(theta1, theta2)
|
||||
|
||||
# Standard: elongated contours → zigzag
|
||||
Z_std = 6 * T1**2 + T2**2
|
||||
axes[0].contour(T1, T2, Z_std, levels=7, colors=GRAY, alpha=0.45, linewidths=0.9)
|
||||
path_std = [(1.6, 1.6), (0.05, 1.1), (0.75, 0.15), (0.03, 0.06), (0, 0)]
|
||||
xs, ys = zip(*path_std)
|
||||
axes[0].plot(xs, ys, "o-", color=C0, lw=1.8, ms=5)
|
||||
axes[0].scatter([0], [0], s=120, color=C1, zorder=5, marker="*")
|
||||
axes[0].set_title("Standard gradient $\\nabla_\\theta \\mathcal{L}$\n(zigzag on ill-conditioned $\\mathcal{I}$)",
|
||||
fontsize=10)
|
||||
axes[0].set_xlabel(r"$\theta_1$"); axes[0].set_ylabel(r"$\theta_2$")
|
||||
|
||||
# Natural: circular contours → direct path
|
||||
Z_nat = T1**2 + T2**2
|
||||
axes[1].contour(T1, T2, Z_nat, levels=7, colors=GRAY, alpha=0.45, linewidths=0.9)
|
||||
path_nat = [(1.6, 1.6), (0.8, 0.8), (0.3, 0.3), (0, 0)]
|
||||
xs2, ys2 = zip(*path_nat)
|
||||
axes[1].plot(xs2, ys2, "o-", color=C2, lw=1.8, ms=5)
|
||||
axes[1].scatter([0], [0], s=120, color=C1, zorder=5, marker="*")
|
||||
axes[1].set_title(r"Natural gradient $\mathcal{I}^{-1}\nabla_\theta\mathcal{L}$" + "\n(direct, reparametrisation-invariant)",
|
||||
fontsize=10)
|
||||
axes[1].set_xlabel(r"$\theta_1$"); axes[1].set_ylabel(r"$\theta_2$")
|
||||
|
||||
plt.tight_layout()
|
||||
save("fig_natural_gradient")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §2.3 PICARD ITERATION
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_picard():
|
||||
t = np.linspace(0, 1.5, 300)
|
||||
# True solution: dx = x dt → x(t) = e^t
|
||||
x_true = np.exp(t)
|
||||
# Picard iterates starting at x0 = 1
|
||||
x0 = np.ones_like(t) # n=0: constant 1
|
||||
x1 = 1 + t # n=1: linear
|
||||
x2 = 1 + t + t**2 / 2 # n=2: quadratic
|
||||
x3 = 1 + t + t**2/2 + t**3/6 # n=3
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 3.8))
|
||||
ax.plot(t, x0, color=GRAY, lw=1.5, ls=":", label=r"$X^{(0)}$: constant")
|
||||
ax.plot(t, x1, color=C3, lw=1.5, ls="-.", label=r"$X^{(1)}$: linear")
|
||||
ax.plot(t, x2, color=C2, lw=1.5, ls="--", label=r"$X^{(2)}$: quadratic")
|
||||
ax.plot(t, x3, color=C1, lw=1.8, label=r"$X^{(3)}$")
|
||||
ax.plot(t, x_true, color=C0, lw=2.2, label=r"$X^{(\infty)} = e^t$ (true)")
|
||||
ax.set_xlabel(r"$t$"); ax.set_ylabel(r"$X^{(n)}_t$")
|
||||
ax.set_title(r"Picard iteration ($dX = X\,dt$, $X_0 = 1$) — successive approximations")
|
||||
ax.legend(fontsize=9); ax.set_ylim(0.8, 5.0)
|
||||
save("fig_picard")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §8 HMM REGIME STATE MACHINE (K = 3)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_hmm_regime():
|
||||
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
|
||||
import matplotlib.patheffects as pe
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 4.2))
|
||||
ax.set_xlim(0, 9); ax.set_ylim(0, 4); ax.axis("off")
|
||||
|
||||
states = [
|
||||
(1.5, 2.6, "State 1\nBull", C2),
|
||||
(4.5, 2.6, "State 2\nNeutral", GRAY),
|
||||
(7.5, 2.6, "State 3\nBear", C3),
|
||||
]
|
||||
box_w, box_h = 2.0, 1.1
|
||||
for (cx, cy, label, col) in states:
|
||||
fancy = FancyBboxPatch((cx - box_w/2, cy - box_h/2), box_w, box_h,
|
||||
boxstyle="round,pad=0.08", linewidth=1.6,
|
||||
edgecolor=col, facecolor=col + "22",
|
||||
zorder=2)
|
||||
ax.add_patch(fancy)
|
||||
ax.text(cx, cy, label, ha="center", va="center", fontsize=10,
|
||||
fontweight="bold", color=col, zorder=3)
|
||||
|
||||
# Forward arrows A₁₂, A₂₃
|
||||
for x0, x1, label in [(2.5, 3.5, r"$A_{12}$"), (5.5, 6.5, r"$A_{23}$")]:
|
||||
ax.annotate("", xy=(x1, 2.85), xytext=(x0, 2.85),
|
||||
arrowprops=dict(arrowstyle="-|>", color=C0, lw=1.5))
|
||||
ax.text((x0+x1)/2, 2.98, label, ha="center", fontsize=9, color=C0)
|
||||
# Backward arrows A₂₁, A₃₂
|
||||
for x0, x1, label in [(3.5, 2.5, r"$A_{21}$"), (6.5, 5.5, r"$A_{32}$")]:
|
||||
ax.annotate("", xy=(x1, 2.35), xytext=(x0, 2.35),
|
||||
arrowprops=dict(arrowstyle="-|>", color=C1, lw=1.5))
|
||||
ax.text((x0+x1)/2, 2.22, label, ha="center", fontsize=9, color=C1)
|
||||
|
||||
# Emission table
|
||||
col_labels = ["State", r"$\mu$", r"$\sigma$", "Character"]
|
||||
rows = [
|
||||
["Bull", "+0.05", "0.12", "high return, low vol"],
|
||||
["Neutral", " 0.00", "0.18", "flat, medium vol"],
|
||||
["Bear", "−0.08", "0.35", "crash, high vol"],
|
||||
]
|
||||
row_colors = [[C2+"33", C2+"33", C2+"33", C2+"33"],
|
||||
[GRAY+"33", GRAY+"33", GRAY+"33", GRAY+"33"],
|
||||
[C3+"33", C3+"33", C3+"33", C3+"33"]]
|
||||
tbl = ax.table(cellText=rows, colLabels=col_labels, loc="bottom",
|
||||
cellColours=row_colors, bbox=[0.05, 0.0, 0.90, 0.42])
|
||||
tbl.auto_set_font_size(False); tbl.set_fontsize(9)
|
||||
for (r, c), cell in tbl.get_celld().items():
|
||||
cell.set_edgecolor("#cccccc")
|
||||
if r == 0:
|
||||
cell.set_facecolor(C0 + "33")
|
||||
cell.set_text_props(fontweight="bold")
|
||||
|
||||
ax.set_title(r"HMM Regime State Machine ($K=3$) — Emission $B_k(y)=\mathcal{N}(\mu_k,\sigma_k^2)$",
|
||||
fontsize=11, pad=6)
|
||||
plt.tight_layout()
|
||||
save("fig_hmm_regime")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §8.2 VITERBI TRELLIS (K=3, T=4)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_viterbi_trellis():
|
||||
from matplotlib.patches import Circle, FancyArrowPatch
|
||||
|
||||
K, T = 3, 4
|
||||
state_labels = ["1 (Bull)", "2 (Neutral)", "3 (Bear)"]
|
||||
map_path = {(1, 1), (1, 2)} # state index 1 = "2 (Neutral)" at t=2,3 (0-indexed t)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 3.8))
|
||||
ax.set_xlim(-0.5, T + 0.5); ax.set_ylim(-0.5, K - 0.3); ax.axis("off")
|
||||
|
||||
# x-positions: t=1..4 → 0.5, 1.5, 2.5, 3.5
|
||||
xs = [0.6 * (t + 1) for t in range(T)]
|
||||
ys = [K - 1 - k for k in range(K)] # top = state 1
|
||||
|
||||
# Draw crossing / passing arrows (selective to show crossing)
|
||||
arrow_kw = dict(arrowstyle="-|>", connectionstyle="arc3,rad=0.0",
|
||||
color=GRAY, lw=1.1, alpha=0.55)
|
||||
cross_kw = dict(arrowstyle="-|>", connectionstyle="arc3,rad=0.18",
|
||||
color=GRAY, lw=1.1, alpha=0.45)
|
||||
for t in range(T - 1):
|
||||
for k in range(K):
|
||||
for k2 in range(K):
|
||||
rad = 0.0 if k == k2 else (0.18 if k2 > k else -0.18)
|
||||
col = C0 if (k == 1 and k2 == 1 and t >= 1) else GRAY
|
||||
alpha = 0.9 if col == C0 else 0.3
|
||||
ax.annotate("", xy=(xs[t+1], ys[k2]), xytext=(xs[t], ys[k]),
|
||||
arrowprops=dict(arrowstyle="-|>",
|
||||
connectionstyle=f"arc3,rad={rad}",
|
||||
color=col, lw=1.2 if col == C0 else 0.8,
|
||||
alpha=alpha))
|
||||
|
||||
# Draw nodes
|
||||
r = 0.14
|
||||
for k in range(K):
|
||||
for t in range(T):
|
||||
is_map = (k == 1 and 1 <= t <= 2)
|
||||
fc = C0 if is_map else "white"
|
||||
ec = C0 if is_map else GRAY
|
||||
circ = Circle((xs[t], ys[k]), r, facecolor=fc, edgecolor=ec, lw=1.8, zorder=4)
|
||||
ax.add_patch(circ)
|
||||
|
||||
# Labels on left
|
||||
for k in range(K):
|
||||
ax.text(-0.1, ys[k], state_labels[k], ha="right", va="center",
|
||||
fontsize=9, color=C0 if k == 1 else "black")
|
||||
|
||||
# x-axis ticks
|
||||
for t in range(T):
|
||||
ax.text(xs[t], -0.35, f"$t={t+1}$", ha="center", va="top", fontsize=9)
|
||||
|
||||
# Legend
|
||||
ax.scatter([], [], color=C0, s=80, label="● MAP (Viterbi) path", zorder=5)
|
||||
ax.scatter([], [], facecolor="white", edgecolors=GRAY, s=80, label="○ other nodes", zorder=5)
|
||||
ax.legend(loc="upper right", fontsize=9, framealpha=0.9)
|
||||
|
||||
ax.set_title(r"Viterbi Trellis ($K=3$, $T=4$) — $\delta_t(k)=\max_j\,\delta_{t-1}(j)\,A_{jk}\,B_k(y_t)$",
|
||||
fontsize=11)
|
||||
plt.tight_layout()
|
||||
save("fig_viterbi_trellis")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §10.2 STANDARD VS NATURAL GRADIENT — PROPERTY COMPARISON
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_std_vs_nat_gradient():
|
||||
from matplotlib.patches import FancyBboxPatch
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(9, 3.0))
|
||||
|
||||
panels = [
|
||||
("Standard Gradient\n" + r"$\theta_{k+1} = \theta_k - \eta\nabla\mathcal{L}$",
|
||||
["Flat $\\mathbb{R}^d$ geometry",
|
||||
"Ignores manifold curvature",
|
||||
"Slow on ill-conditioned $\\mathcal{I}$",
|
||||
"$O(\\kappa(\\mathcal{I}))$ iterations"],
|
||||
C3, C3 + "18"),
|
||||
("Natural Gradient\n" + r"$\theta_{k+1} = \theta_k - \eta\,\mathcal{I}(\theta)^{-1}\nabla\mathcal{L}$",
|
||||
["Riemannian metric $\\mathcal{I}(\\theta)$",
|
||||
"Adapts to manifold geometry",
|
||||
"Reparametrisation-invariant",
|
||||
"$O(1)$ on exp. families (MLE step)"],
|
||||
C2, C2 + "18"),
|
||||
]
|
||||
|
||||
for ax, (title, props, border, bg) in zip(axes, panels):
|
||||
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
|
||||
fancy = FancyBboxPatch((0.03, 0.04), 0.94, 0.92,
|
||||
boxstyle="round,pad=0.04", linewidth=2,
|
||||
edgecolor=border, facecolor=bg)
|
||||
ax.add_patch(fancy)
|
||||
ax.text(0.5, 0.87, title, ha="center", va="top", fontsize=10,
|
||||
fontweight="bold", color=border, transform=ax.transAxes,
|
||||
multialignment="center")
|
||||
y = 0.68
|
||||
for prop in props:
|
||||
ax.text(0.12, y, "• " + prop, ha="left", va="top", fontsize=9.5,
|
||||
transform=ax.transAxes, color="#222222")
|
||||
y -= 0.17
|
||||
|
||||
fig.suptitle("Standard vs Natural Gradient — geometric properties", fontsize=11, y=1.02)
|
||||
plt.tight_layout()
|
||||
save("fig_std_vs_nat_gradient")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# §10.3 MATRIX LIE GROUP HIERARCHY
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def fig_lie_group_hierarchy():
|
||||
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 4.6))
|
||||
ax.set_xlim(0, 9); ax.set_ylim(0, 4.6); ax.axis("off")
|
||||
|
||||
nodes = {
|
||||
"GL": (4.5, 4.1, r"$\mathrm{GL}(n,\mathbb{R})$" + "\nall invertible $n\times n$", C0),
|
||||
"SL": (1.8, 2.85, r"$\mathrm{SL}(n,\mathbb{R})$" + "\n" + r"$\det=1$", C2),
|
||||
"On": (4.5, 2.85, r"$O(n)$" + "\n$R^\top R=I$", C1),
|
||||
"Sp": (7.2, 2.85, r"$\mathrm{Sp}(2n,\mathbb{R})$" + "\npreserves " + r"$\omega$", C2),
|
||||
"SO": (4.5, 1.55, r"$\mathrm{SO}(n)$" + "\n" + r"$\det=+1$ (rotations)", C2),
|
||||
"Hn": (1.8, 1.55, r"$H(n)$ Heisenberg" + "\nupper triangular", C3),
|
||||
}
|
||||
notes = {
|
||||
"SO": "portfolio factor\nrotation, PCA",
|
||||
"Sp": "Hamiltonian\nmechanics, PMP",
|
||||
"Hn": "path-signature\nfeature maps",
|
||||
}
|
||||
edges = [("GL","SL"), ("GL","On"), ("GL","Sp"), ("On","SO")]
|
||||
|
||||
bw, bh = 2.2, 0.76
|
||||
for key, (cx, cy, label, col) in nodes.items():
|
||||
fbp = FancyBboxPatch((cx-bw/2, cy-bh/2), bw, bh,
|
||||
boxstyle="round,pad=0.07", lw=1.6,
|
||||
edgecolor=col, facecolor=col+"22", zorder=2)
|
||||
ax.add_patch(fbp)
|
||||
ax.text(cx, cy, label, ha="center", va="center", fontsize=8.5,
|
||||
multialignment="center", color=col, fontweight="bold", zorder=3)
|
||||
if key in notes:
|
||||
ax.text(cx + bw/2 + 0.15, cy, notes[key], va="center",
|
||||
fontsize=7.5, color="#555555", fontstyle="italic")
|
||||
|
||||
for src, dst in edges:
|
||||
sx, sy = nodes[src][0], nodes[src][1]
|
||||
dx, dy = nodes[dst][0], nodes[dst][1]
|
||||
ax.annotate("", xy=(dx, dy + bh/2 + 0.04), xytext=(sx, sy - bh/2 - 0.04),
|
||||
arrowprops=dict(arrowstyle="-|>", color=GRAY, lw=1.4))
|
||||
|
||||
ax.set_title("Matrix Lie Group Hierarchy — subgroup inclusions and finance applications",
|
||||
fontsize=11, pad=5)
|
||||
plt.tight_layout()
|
||||
save("fig_lie_group_hierarchy")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# RUN ALL
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if __name__ == "__main__":
|
||||
funcs = [
|
||||
fig_de_mutation, fig_rastrigin,
|
||||
fig_random_walk, fig_bm_fan, fig_gbm,
|
||||
fig_ito_correction,
|
||||
fig_picard,
|
||||
fig_fokker_planck, fig_em_milstein,
|
||||
fig_ou_path, fig_ou_transition, fig_ou_loglik, fig_ou_residuals,
|
||||
fig_poisson, fig_jump_diffusion, fig_levy_tails,
|
||||
fig_kalman_covariance,
|
||||
fig_mcmc_energy, fig_mcmc_trace,
|
||||
fig_kl_asymmetry, fig_fisher_curvature,
|
||||
fig_curvatures, fig_natural_gradient,
|
||||
# new §8 & §10 diagrams
|
||||
fig_hmm_regime, fig_viterbi_trellis,
|
||||
fig_std_vs_nat_gradient, fig_lie_group_hierarchy,
|
||||
]
|
||||
for fn in funcs:
|
||||
print(f" {fn.__name__} ... ", end="", flush=True)
|
||||
fn()
|
||||
print("ok")
|
||||
print(f"\nDone — {len(funcs)} SVGs saved to {OUT}")
|
||||
@@ -0,0 +1,138 @@
|
||||
/* ─── Optimiz-rs Documentation Custom Styles ──────────────────────────────── */
|
||||
/* Brand: orange (#f97316) on furo dark theme */
|
||||
|
||||
:root {
|
||||
--brand-orange: #f97316;
|
||||
--brand-orange-light: #fb923c;
|
||||
--brand-orange-dim: rgba(249, 115, 22, 0.15);
|
||||
--brand-orange-border: rgba(249, 115, 22, 0.35);
|
||||
}
|
||||
|
||||
/* ── Mermaid diagram container ─────────────────────────────────────────────── */
|
||||
.mermaid {
|
||||
background: transparent !important;
|
||||
padding: 1.5rem 0;
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mermaid svg {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
filter: drop-shadow(0 4px 16px rgba(249, 115, 22, 0.12));
|
||||
}
|
||||
|
||||
/* ── Better admonitions ─────────────────────────────────────────────────────── */
|
||||
.admonition {
|
||||
border-radius: 10px !important;
|
||||
border-left-width: 4px !important;
|
||||
margin: 1.5rem 0 !important;
|
||||
}
|
||||
|
||||
.admonition.note {
|
||||
border-left-color: var(--brand-orange) !important;
|
||||
background: var(--brand-orange-dim) !important;
|
||||
}
|
||||
|
||||
.admonition.tip {
|
||||
border-left-color: #22c55e !important;
|
||||
background: rgba(34, 197, 94, 0.1) !important;
|
||||
}
|
||||
|
||||
.admonition.warning {
|
||||
border-left-color: #f59e0b !important;
|
||||
background: rgba(245, 158, 11, 0.1) !important;
|
||||
}
|
||||
|
||||
.admonition.important {
|
||||
border-left-color: #ef4444 !important;
|
||||
background: rgba(239, 68, 68, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ── Section headers ────────────────────────────────────────────────────────── */
|
||||
h1, h2, h3 {
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-bottom: 2px solid var(--brand-orange-border);
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* ── Code blocks ────────────────────────────────────────────────────────────── */
|
||||
.highlight {
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
code.literal {
|
||||
background: rgba(249, 115, 22, 0.08) !important;
|
||||
border: 1px solid var(--brand-orange-border) !important;
|
||||
padding: 0.1em 0.4em !important;
|
||||
border-radius: 4px !important;
|
||||
color: var(--brand-orange-light) !important;
|
||||
font-size: 0.88em !important;
|
||||
}
|
||||
|
||||
/* ── Tables ─────────────────────────────────────────────────────────────────── */
|
||||
table.docutils {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1.2rem 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
table.docutils th {
|
||||
background: var(--brand-orange-dim) !important;
|
||||
color: var(--brand-orange-light) !important;
|
||||
font-weight: 700;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-bottom: 2px solid var(--brand-orange-border);
|
||||
}
|
||||
|
||||
table.docutils td {
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
table.docutils tr:hover td {
|
||||
background: rgba(249, 115, 22, 0.04);
|
||||
}
|
||||
|
||||
/* ── Performance metric boxes (for benchmarks) ──────────────────────────────── */
|
||||
.perf-box {
|
||||
background: linear-gradient(135deg, rgba(249,115,22,0.12) 0%, rgba(249,115,22,0.04) 100%);
|
||||
border: 1px solid var(--brand-orange-border);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 1rem 0;
|
||||
display: inline-block;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.perf-box .val {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: var(--brand-orange);
|
||||
}
|
||||
|
||||
.perf-box .lbl {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* ── Navigation sidebar ─────────────────────────────────────────────────────── */
|
||||
.sidebar-tree .current > .reference {
|
||||
color: var(--brand-orange) !important;
|
||||
}
|
||||
|
||||
/* ── Mobile ─────────────────────────────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.mermaid svg { width: 100% !important; }
|
||||
table.docutils { font-size: 0.78rem; }
|
||||
}
|
||||
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,494 @@
|
||||
# Differential Evolution
|
||||
|
||||
**Differential Evolution (DE)** is a population-based metaheuristic optimization algorithm
|
||||
introduced by Storn and Price (1997). It is particularly effective for continuous, non-convex,
|
||||
multimodal optimization problems where gradient information is unavailable or unreliable.
|
||||
|
||||
This module provides a high-performance Rust implementation with Python bindings, supporting
|
||||
multiple mutation strategies, adaptive parameter control (jDE), and parallel evaluation.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### Problem Formulation
|
||||
|
||||
DE solves unconstrained (or box-constrained) minimization problems:
|
||||
|
||||
$$
|
||||
\min_{\mathbf{x} \in \mathbb{R}^D} f(\mathbf{x})
|
||||
$$
|
||||
|
||||
subject to box constraints:
|
||||
|
||||
$$
|
||||
x_j \in [l_j, u_j], \quad j = 1, \ldots, D
|
||||
$$
|
||||
|
||||
**DE is well-suited when:**
|
||||
|
||||
- $f$ is continuous but non-differentiable
|
||||
- Multiple local minima exist
|
||||
- Gradient information is unavailable or expensive
|
||||
- Problem dimension is moderate ($D < 100$)
|
||||
|
||||
---
|
||||
|
||||
### Population
|
||||
|
||||
DE maintains a population of $N_P$ candidate solutions:
|
||||
|
||||
$$
|
||||
P_g = \{\mathbf{x}_{1,g}, \mathbf{x}_{2,g}, \ldots, \mathbf{x}_{N_P,g}\}
|
||||
$$
|
||||
|
||||
where $g$ is the generation number and $\mathbf{x}_{i,g} \in \mathbb{R}^D$.
|
||||
|
||||
**Rule of thumb:** $N_P = 10 \times D$ where $D$ is the problem dimension.
|
||||
|
||||
---
|
||||
|
||||
### Main Loop
|
||||
|
||||
For each generation $g = 0, 1, 2, \ldots$:
|
||||
|
||||
1. **Mutation**: Create mutant vectors by combining existing solutions
|
||||
2. **Crossover**: Mix mutant with target vector to form trial vector
|
||||
3. **Selection**: Keep better solution (greedy selection)
|
||||
|
||||
---
|
||||
|
||||
## Mutation Strategies
|
||||
|
||||
The mutation operator creates a **mutant vector** $\mathbf{v}_{i,g+1}$ from existing
|
||||
population members:
|
||||
|
||||
### DE/rand/1 (Classic Strategy)
|
||||
|
||||
$$
|
||||
\mathbf{v}_{i,g+1} = \mathbf{x}_{r_1,g} + F \cdot (\mathbf{x}_{r_2,g} - \mathbf{x}_{r_3,g})
|
||||
$$
|
||||
|
||||
where:
|
||||
- $r_1, r_2, r_3 \in \{1, \ldots, N_P\}$ are randomly chosen, distinct, and $\neq i$
|
||||
- $F \in (0, 2]$ is the **mutation factor** (typically 0.5–1.0)
|
||||
|
||||
**Interpretation:** Start from a random population member $\mathbf{x}_{r_1}$,
|
||||
move in direction given by the difference $(\mathbf{x}_{r_2} - \mathbf{x}_{r_3})$,
|
||||
scaled by $F$.
|
||||
|
||||
**Characteristics:** Most explorative, good for diverse populations.
|
||||
|
||||
### DE/best/1
|
||||
|
||||
$$
|
||||
\mathbf{v}_{i,g+1} = \mathbf{x}_{\text{best},g} + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g})
|
||||
$$
|
||||
|
||||
**Advantage:** Faster convergence toward the best-known solution.
|
||||
|
||||
**Disadvantage:** More likely to get stuck in local minima.
|
||||
|
||||
### DE/current-to-best/1
|
||||
|
||||
$$
|
||||
\mathbf{v}_{i,g+1} = \mathbf{x}_{i,g} + F \cdot (\mathbf{x}_{\text{best},g} - \mathbf{x}_{i,g}) + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g})
|
||||
$$
|
||||
|
||||
**Interpretation:** Move current solution toward the best while also exploring.
|
||||
|
||||
**Characteristics:** Balanced exploration/exploitation.
|
||||
|
||||
### DE/rand/2
|
||||
|
||||
$$
|
||||
\mathbf{v}_{i,g+1} = \mathbf{x}_{r_1,g} + F \cdot (\mathbf{x}_{r_2,g} - \mathbf{x}_{r_3,g}) + F \cdot (\mathbf{x}_{r_4,g} - \mathbf{x}_{r_5,g})
|
||||
$$
|
||||
|
||||
**Characteristics:** More disruptive, better for highly multimodal problems.
|
||||
|
||||
### DE/best/2
|
||||
|
||||
$$
|
||||
\mathbf{v}_{i,g+1} = \mathbf{x}_{\text{best},g} + F \cdot (\mathbf{x}_{r_1,g} - \mathbf{x}_{r_2,g}) + F \cdot (\mathbf{x}_{r_3,g} - \mathbf{x}_{r_4,g})
|
||||
$$
|
||||
|
||||
**Characteristics:** Aggressive convergence to the best solution.
|
||||
|
||||
---
|
||||
|
||||
## Crossover
|
||||
|
||||
After mutation, the **trial vector** $\mathbf{u}_{i,g+1}$ is formed by mixing
|
||||
components from the mutant and the target vector.
|
||||
|
||||
### Binomial Crossover
|
||||
|
||||
For each component $j = 1, \ldots, D$:
|
||||
|
||||
$$
|
||||
u_{i,j,g+1} = \begin{cases}
|
||||
v_{i,j,g+1} & \text{if } \text{rand}(0,1) \leq CR \text{ or } j = j_{\text{rand}} \\
|
||||
x_{i,j,g} & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
where:
|
||||
- $CR \in [0, 1]$ is the **crossover probability**
|
||||
- $j_{\text{rand}} \in \{1, \ldots, D\}$ ensures at least one component comes from the mutant
|
||||
|
||||
**Effect:** $CR$ controls how much of the mutant vector is used.
|
||||
|
||||
| CR Value | Effect |
|
||||
|----------|--------|
|
||||
| Low (0.1–0.3) | Less information exchange, slower convergence. Better for separable problems |
|
||||
| High (0.7–0.9) | More information exchange, faster convergence. Better for non-separable problems |
|
||||
| 0.0 | Pure mutation (except $j_{\text{rand}}$) |
|
||||
| 1.0 | Full crossover |
|
||||
|
||||
---
|
||||
|
||||
## Selection
|
||||
|
||||
Greedy selection (for minimization):
|
||||
|
||||
$$
|
||||
\mathbf{x}_{i,g+1} = \begin{cases}
|
||||
\mathbf{u}_{i,g+1} & \text{if } f(\mathbf{u}_{i,g+1}) \leq f(\mathbf{x}_{i,g}) \\
|
||||
\mathbf{x}_{i,g} & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
**Property:** Population quality never decreases:
|
||||
|
||||
$$
|
||||
f(\mathbf{x}_{\text{best},g+1}) \leq f(\mathbf{x}_{\text{best},g})
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Complete Algorithm
|
||||
|
||||
```{mermaid}
|
||||
flowchart TD
|
||||
A["🎲 Initialize Population\nx_i = l + rand · (u − l)"] --> B["📊 Evaluate Fitness\nf_i = f(x_i) for all i"]
|
||||
B --> C{{"g < max_iter?"}}
|
||||
C -->|Yes| D["Mutation\nv = x_r1 + F · (x_r2 − x_r3)"]
|
||||
D --> E["Crossover\nu_j = v_j if rand ≤ CR or j = j_rand\nelse u_j = x_j"]
|
||||
E --> F["Clip to bounds [l, u]"]
|
||||
F --> G{{"f(trial) ≤ f(target)?"}}
|
||||
G -->|"Yes — better"| H["✅ Accept trial\nx_i ← u_i"]
|
||||
G -->|"No — worse"| I["Keep current\nx_i unchanged"]
|
||||
H & I --> C
|
||||
C -->|No| J["🏆 Return x_best, f(x_best)"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parameter Selection Guidelines
|
||||
|
||||
### Population Size ($N_P$)
|
||||
|
||||
| Size Category | Range | Use Case |
|
||||
|---------------|-------|----------|
|
||||
| Small | < 4D | Faster convergence; risk premature convergence. Simple unimodal problems |
|
||||
| Medium | 10D (default) | Good balance for most problems |
|
||||
| Large | > 20D | Better exploration; slower convergence. Highly multimodal problems |
|
||||
|
||||
**Minimum:** $N_P \geq 4$ (needed for mutation with three distinct indices).
|
||||
|
||||
### Mutation Factor ($F$)
|
||||
|
||||
| F Value | Effect |
|
||||
|---------|--------|
|
||||
| Low (0.4–0.6) | Fine-tuning, local search. Safer, less disruptive |
|
||||
| High (0.8–1.2) | Exploration, global search. Escape local minima |
|
||||
|
||||
**Typical range:** $F \in [0.4, 1.0]$, default 0.8.
|
||||
|
||||
### Crossover Probability ($CR$)
|
||||
|
||||
| CR Value | Effect |
|
||||
|----------|--------|
|
||||
| Low (0.1–0.3) | Best for separable problems |
|
||||
| High (0.7–0.9) | Best for non-separable problems |
|
||||
|
||||
**Default:** 0.7–0.9 for most problems.
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import differential_evolution
|
||||
|
||||
def rastrigin(x):
|
||||
"""Multimodal benchmark function with many local minima."""
|
||||
A = 10
|
||||
return A * len(x) + sum(x**2 - A * np.cos(2 * np.pi * x))
|
||||
|
||||
best_x, best_fx = differential_evolution(
|
||||
objective_fn=rastrigin,
|
||||
bounds=[(-5.12, 5.12)] * 10, # 10-dimensional problem
|
||||
strategy="best1",
|
||||
popsize=20,
|
||||
maxiter=500,
|
||||
adaptive=True,
|
||||
)
|
||||
|
||||
print(f"Best fitness: {best_fx:.6f}")
|
||||
print(f"Best solution: {best_x}")
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Best fitness: 0.000042
|
||||
Best solution: [ 0.00012 -0.00023 0.00018 ... ]
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```python
|
||||
from optimizr import DifferentialEvolution
|
||||
|
||||
de = DifferentialEvolution(
|
||||
bounds=[(-5, 5)] * 20,
|
||||
strategy="rand1", # mutation strategy
|
||||
popsize=200, # population size
|
||||
maxiter=1000, # maximum generations
|
||||
F=0.8, # mutation factor
|
||||
CR=0.9, # crossover probability
|
||||
tol=1e-8, # convergence tolerance
|
||||
seed=42, # reproducibility
|
||||
)
|
||||
|
||||
result = de.minimize(sphere_function)
|
||||
print(f"Converged in {result.nit} iterations")
|
||||
print(f"Function evaluations: {result.nfev}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adaptive Control (jDE)
|
||||
|
||||
Optimiz-rs implements **jDE** (self-adaptive DE), where the parameters $F$ and $CR$
|
||||
evolve with the population:
|
||||
|
||||
$$
|
||||
F_{i,g+1} = \begin{cases}
|
||||
F_l + \text{rand}(0,1) \cdot (F_u - F_l) & \text{if } \text{rand}(0,1) < \tau_1 \\
|
||||
F_{i,g} & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
$$
|
||||
CR_{i,g+1} = \begin{cases}
|
||||
\text{rand}(0,1) & \text{if } \text{rand}(0,1) < \tau_2 \\
|
||||
CR_{i,g} & \text{otherwise}
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
**Enable jDE:**
|
||||
|
||||
```python
|
||||
de = DifferentialEvolution(
|
||||
bounds=[(-5, 5)] * 20,
|
||||
adaptive=True, # enables jDE
|
||||
tau_F=0.1, # probability of F mutation
|
||||
tau_CR=0.1, # probability of CR mutation
|
||||
)
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- No need to manually tune $F$ and $CR$
|
||||
- Adapts to problem landscape during optimization
|
||||
- Generally robust across problem types
|
||||
|
||||
---
|
||||
|
||||
## Parallel Evaluation (Rust Backend)
|
||||
|
||||
For pure-Rust objectives or when Python callbacks are not needed, enable
|
||||
data-parallel evaluation via Rayon:
|
||||
|
||||
```python
|
||||
from optimizr import parallel_differential_evolution_rust
|
||||
|
||||
result = parallel_differential_evolution_rust(
|
||||
objective="rastrigin", # built-in benchmark
|
||||
dim=50,
|
||||
bounds=(-5.12, 5.12),
|
||||
popsize=500,
|
||||
maxiter=2000,
|
||||
n_threads=8,
|
||||
)
|
||||
|
||||
print(f"Best fitness: {result.best_fitness:.8f}")
|
||||
```
|
||||
|
||||
**Speedup:** Near-linear up to $N_P$ processors for expensive objectives.
|
||||
|
||||
---
|
||||
|
||||
## Convergence Analysis
|
||||
|
||||
### Theoretical Properties
|
||||
|
||||
**Global Convergence Theorem** (Lampinen, 2001):
|
||||
|
||||
Under these sufficient conditions:
|
||||
- Population size $N_P > 3$
|
||||
- Mutation factor $F > 0$
|
||||
- At least one component crossed over ($j_{\text{rand}}$)
|
||||
|
||||
DE is a **global optimization method**: any point can be reached with positive probability.
|
||||
|
||||
### Diversity Measure
|
||||
|
||||
$$
|
||||
D_g = \frac{1}{N_P D} \sum_{i=1}^{N_P} \sum_{j=1}^D |x_{i,j,g} - \bar{x}_{j,g}|
|
||||
$$
|
||||
|
||||
| Diversity | Behavior |
|
||||
|-----------|----------|
|
||||
| High | Exploration (global search) |
|
||||
| Low | Exploitation (local search) |
|
||||
|
||||
### Empirical Budget
|
||||
|
||||
**Rule of thumb:** Budget $10^4 \times D$ function evaluations for moderately difficult problems.
|
||||
|
||||
---
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Algorithm | Gradient | Global | Constraints | Speed | Best For |
|
||||
|-----------|----------|--------|-------------|-------|----------|
|
||||
| **DE** | No | Yes | Box | Medium | Non-convex, continuous |
|
||||
| Gradient Descent | Yes | No | Yes | Fast | Smooth, convex |
|
||||
| Genetic Algorithm | No | Yes | Yes | Slow | Discrete, combinatorial |
|
||||
| Particle Swarm | No | Yes | Box | Fast | Continuous, many dimensions |
|
||||
| CMA-ES | No | Yes | Box | Fast | Continuous, noisy |
|
||||
|
||||
---
|
||||
|
||||
## Practical Tips
|
||||
|
||||
### 1. Start Simple
|
||||
|
||||
Use defaults: $N_P = 10D$, $F = 0.8$, $CR = 0.7$, `strategy="rand1"`.
|
||||
|
||||
### 2. Scale Variables
|
||||
|
||||
Normalize parameters to similar ranges for better performance.
|
||||
|
||||
### 3. Warm Start
|
||||
|
||||
If you have a good initial guess, seed the population around it.
|
||||
|
||||
### 4. Hybrid Approach
|
||||
|
||||
Use DE for global search, then a local optimizer for refinement:
|
||||
|
||||
```python
|
||||
# Global search with DE
|
||||
best_x, _ = differential_evolution(f, bounds, maxiter=200)
|
||||
|
||||
# Local refinement with L-BFGS-B
|
||||
from scipy.optimize import minimize
|
||||
result = minimize(f, best_x, method='L-BFGS-B', bounds=bounds)
|
||||
```
|
||||
|
||||
### 5. Monitor Convergence
|
||||
|
||||
Plot:
|
||||
- Best fitness vs. generation
|
||||
- Average population fitness vs. generation
|
||||
- Population diversity vs. generation
|
||||
|
||||
### 6. Restarts
|
||||
|
||||
If premature convergence detected, restart with new random population.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Slow convergence | $F$ or $CR$ too low | Increase $F$ to 0.8, $CR$ to 0.9 |
|
||||
| Premature convergence | Population too small | Increase $N_P$ to 15–20D |
|
||||
| Oscillating fitness | $F$ too high | Decrease $F$ to 0.5–0.6 |
|
||||
| Stuck in local minimum | Using `best1` strategy | Switch to `rand1` or `rand2` |
|
||||
|
||||
---
|
||||
|
||||
## Benchmark Results
|
||||
|
||||
Performance on standard test functions (D=30, $N_P=300$, 1000 generations):
|
||||
|
||||
| Function | Best Fitness | Iterations | Time (s) |
|
||||
|----------|-------------|------------|----------|
|
||||
| Sphere | 1.2e-28 | 412 | 0.8 |
|
||||
| Rosenbrock | 2.4e-08 | 891 | 1.4 |
|
||||
| Rastrigin | 4.1e-05 | 1000 | 2.1 |
|
||||
| Ackley | 8.8e-15 | 623 | 1.2 |
|
||||
| Griewank | 3.7e-12 | 548 | 1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Advantages & Limitations
|
||||
|
||||
### Advantages
|
||||
|
||||
✅ No gradient information needed
|
||||
|
||||
✅ Handles non-convex, multimodal functions well
|
||||
|
||||
✅ Few parameters to tune
|
||||
|
||||
✅ Simple to implement and understand
|
||||
|
||||
✅ Robust across problem types
|
||||
|
||||
✅ Naturally handles box constraints
|
||||
|
||||
✅ Population maintains diversity
|
||||
|
||||
### Limitations
|
||||
|
||||
❌ Slower than gradient methods (when gradients are available)
|
||||
|
||||
❌ Scales poorly to high dimensions ($D > 100$)
|
||||
|
||||
❌ No convergence guarantees in finite time
|
||||
|
||||
❌ Requires many function evaluations
|
||||
|
||||
❌ Performance sensitive to parameter choices
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Storn, R. & Price, K. (1997). "Differential evolution – A simple and efficient heuristic for global optimization over continuous spaces." *Journal of Global Optimization*, 11(4):341–359.
|
||||
|
||||
2. Price, K., Storn, R.M. & Lampinen, J.A. (2005). *Differential Evolution: A Practical Approach to Global Optimization*. Springer.
|
||||
|
||||
3. Das, S. & Suganthan, P.N. (2011). "Differential evolution: A survey of the state-of-the-art." *IEEE Transactions on Evolutionary Computation*, 15(1):4–31.
|
||||
|
||||
4. Brest, J. et al. (2006). "Self-adapting control parameters in differential evolution." *IEEE Trans. Evolutionary Computation*, 10(6):646–657. (jDE)
|
||||
|
||||
5. Tanabe, R. & Fukunaga, A. (2013). "Success-history based parameter adaptation for differential evolution." *IEEE CEC*, pp. 71–78. (SHADE)
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Grid Search](grid_search.md) – Exhaustive search for small parameter spaces
|
||||
- [MCMC](mcmc.md) – Sampling-based inference for Bayesian optimization
|
||||
- [Mean Field Games](mean_field_games.md) – Population dynamics optimization
|
||||
@@ -0,0 +1,52 @@
|
||||
Graph Laplacians and Spectral Clustering
|
||||
========================================
|
||||
|
||||
The module :code:`graph` provides graph Laplacian operators and a
|
||||
spectral clustering algorithm built on a Jacobi diagonaliser.
|
||||
|
||||
Laplacians
|
||||
----------
|
||||
|
||||
For a weighted undirected graph with adjacency matrix :math:`W \in \mathbb{R}^{n\times n}_{\ge 0}`
|
||||
and degree matrix :math:`D = \mathrm{diag}(W \mathbf{1})`:
|
||||
|
||||
- **Combinatorial**: :math:`L = D - W`.
|
||||
- **Symmetric normalised**: :math:`L_{\mathrm{sym}} = I - D^{-1/2} W D^{-1/2}`.
|
||||
- **Random-walk normalised**: :math:`L_{\mathrm{rw}} = I - D^{-1} W`.
|
||||
|
||||
Each operator is positive semidefinite and the multiplicity of the
|
||||
zero eigenvalue equals the number of connected components.
|
||||
|
||||
Spectral Clustering
|
||||
-------------------
|
||||
|
||||
Given :math:`W` and a target number of clusters :math:`k`:
|
||||
|
||||
1. Build :math:`L_{\mathrm{sym}}` (or another Laplacian).
|
||||
2. Diagonalise via cyclic Jacobi rotations to obtain the eigenpairs
|
||||
:math:`(\lambda_i, u_i)`.
|
||||
3. Stack the :math:`k` eigenvectors associated with the smallest
|
||||
eigenvalues as columns of :math:`U \in \mathbb{R}^{n \times k}`.
|
||||
4. Normalise rows of :math:`U` and run Lloyd's algorithm with
|
||||
k-means++ initialisation on the rows.
|
||||
|
||||
The Fiedler eigenvalue :math:`\lambda_2` is reported separately as a
|
||||
proxy for the spectral gap.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub enum LaplacianKind { Combinatorial, SymmetricNormalised, RandomWalk }
|
||||
pub fn combinatorial_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
|
||||
pub fn normalised_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
|
||||
pub fn random_walk_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
|
||||
|
||||
pub struct SpectralClusterResult {
|
||||
pub labels: Vec<usize>,
|
||||
pub eigenvalues: Vec<f64>,
|
||||
pub fiedler_value: f64,
|
||||
}
|
||||
pub fn spectral_cluster(w: ArrayView2<f64>, k: usize, n_kmeans_iter: usize, seed: u64)
|
||||
-> Result<SpectralClusterResult>;
|
||||
@@ -0,0 +1,492 @@
|
||||
# Grid Search
|
||||
|
||||
**Grid Search** (also called parameter sweep) is a deterministic hyperparameter optimization
|
||||
method that exhaustively evaluates all combinations of parameter values from a predefined grid.
|
||||
While simple, it provides guaranteed coverage of the search space and is ideal for
|
||||
low-dimensional problems.
|
||||
|
||||
This module provides a fast implementation with optional Rust acceleration for
|
||||
Cartesian product generation and parallel evaluation.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### Problem Formulation
|
||||
|
||||
Given an objective function $f(\theta)$ and a discrete parameter grid:
|
||||
|
||||
$$
|
||||
\Theta = \Theta_1 \times \Theta_2 \times \cdots \times \Theta_D
|
||||
$$
|
||||
|
||||
where $\Theta_i = \{\theta_{i,1}, \theta_{i,2}, \ldots, \theta_{i,n_i}\}$ is the set of
|
||||
candidate values for parameter $i$.
|
||||
|
||||
**Objective:** Find the optimal parameters:
|
||||
|
||||
$$
|
||||
\theta^* = \arg\min_{\theta \in \Theta} f(\theta)
|
||||
$$
|
||||
|
||||
### Total Evaluations
|
||||
|
||||
The number of function evaluations grows as the **Cartesian product**:
|
||||
|
||||
$$
|
||||
|\Theta| = \prod_{i=1}^{D} n_i
|
||||
$$
|
||||
|
||||
where $n_i = |\Theta_i|$ is the number of values for parameter $i$.
|
||||
|
||||
| Parameters | Values Each | Total Evaluations |
|
||||
|------------|-------------|-------------------|
|
||||
| 2 | 5 | 25 |
|
||||
| 3 | 5 | 125 |
|
||||
| 4 | 5 | 625 |
|
||||
| 5 | 5 | 3,125 |
|
||||
| 3 | 10 | 1,000 |
|
||||
| 5 | 10 | 100,000 |
|
||||
|
||||
**Warning:** Grid search suffers from the **curse of dimensionality**. Use sparingly
|
||||
for $D > 4$ or when function evaluations are expensive.
|
||||
|
||||
---
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
Algorithm: Grid Search
|
||||
──────────────────────
|
||||
Input: objective f, parameter grid Θ = Θ₁ × Θ₂ × ... × Θ_D
|
||||
|
||||
1. Generate all combinations: C = Θ₁ × Θ₂ × ... × Θ_D
|
||||
|
||||
2. Initialize: best_score = ∞, best_params = None
|
||||
|
||||
3. For each θ in C:
|
||||
a. Evaluate: score = f(θ)
|
||||
b. If score < best_score:
|
||||
best_score = score
|
||||
best_params = θ
|
||||
|
||||
4. Return best_params, best_score
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Grid Search
|
||||
|
||||
### Good Use Cases
|
||||
|
||||
✅ **Few parameters** (D ≤ 3–4)
|
||||
|
||||
✅ **Coarse exploration** before fine-tuning
|
||||
|
||||
✅ **Discrete parameters** (e.g., layer counts, categoricals)
|
||||
|
||||
✅ **Reproducibility required** (deterministic)
|
||||
|
||||
✅ **Parameter interactions** need full coverage
|
||||
|
||||
✅ **Fast objectives** (< 1 second per evaluation)
|
||||
|
||||
### Poor Use Cases
|
||||
|
||||
❌ **Many parameters** (D > 5) — exponential explosion
|
||||
|
||||
❌ **Continuous parameters** — wastes evaluations between grid points
|
||||
|
||||
❌ **Expensive objectives** — better to use adaptive methods
|
||||
|
||||
❌ **High-resolution search** — consider random search or DE
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from optimizr import grid_search
|
||||
|
||||
# Objective returns a scalar score (lower is better)
|
||||
def objective(params):
|
||||
lr = params["lr"]
|
||||
dropout = params["dropout"]
|
||||
# Simulate validation loss
|
||||
return (lr - 0.02)**2 + (dropout - 0.1)**2
|
||||
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn=objective,
|
||||
param_grid={
|
||||
"lr": [0.005, 0.01, 0.02, 0.05, 0.1],
|
||||
"dropout": [0.0, 0.05, 0.1, 0.2, 0.3],
|
||||
},
|
||||
)
|
||||
|
||||
print(f"Best parameters: {best_params}")
|
||||
print(f"Best score: {best_score:.6f}")
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Best parameters: {'lr': 0.02, 'dropout': 0.1}
|
||||
Best score: 0.000000
|
||||
```
|
||||
|
||||
### With Verbose Logging
|
||||
|
||||
```python
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn=objective,
|
||||
param_grid=param_grid,
|
||||
verbose=True, # print progress
|
||||
)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
[1/25] lr=0.005, dropout=0.0 → score=0.0127
|
||||
[2/25] lr=0.005, dropout=0.05 → score=0.0102
|
||||
...
|
||||
[15/25] lr=0.02, dropout=0.1 → score=0.0000 [BEST]
|
||||
...
|
||||
```
|
||||
|
||||
### Parallel Evaluation
|
||||
|
||||
For independent, thread-safe objectives:
|
||||
|
||||
```python
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn=objective,
|
||||
param_grid=param_grid,
|
||||
n_jobs=4, # parallel workers
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** Objective function must be thread-safe (no shared mutable state).
|
||||
|
||||
---
|
||||
|
||||
## Grid Construction Strategies
|
||||
|
||||
### Linear Grid
|
||||
|
||||
Evenly spaced values:
|
||||
|
||||
```python
|
||||
param_grid = {
|
||||
"lr": [0.001, 0.005, 0.01, 0.05, 0.1], # linear
|
||||
}
|
||||
```
|
||||
|
||||
### Logarithmic Grid
|
||||
|
||||
For parameters spanning orders of magnitude:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
param_grid = {
|
||||
"lr": list(np.logspace(-4, -1, 10)), # 1e-4 to 1e-1
|
||||
"weight_decay": list(np.logspace(-5, -2, 8)),
|
||||
}
|
||||
```
|
||||
|
||||
### Mixed Types
|
||||
|
||||
```python
|
||||
param_grid = {
|
||||
"lr": [0.001, 0.01, 0.1], # continuous
|
||||
"batch_size": [16, 32, 64, 128], # integer
|
||||
"optimizer": ["adam", "sgd", "rmsprop"], # categorical
|
||||
"use_bn": [True, False], # boolean
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Example: Neural Network Tuning
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import grid_search
|
||||
|
||||
def train_and_evaluate(params):
|
||||
"""
|
||||
Train a neural network with given hyperparameters
|
||||
and return validation loss.
|
||||
"""
|
||||
lr = params["lr"]
|
||||
dropout = params["dropout"]
|
||||
hidden_size = params["hidden_size"]
|
||||
|
||||
# Simulated training (replace with real training loop)
|
||||
# In practice: build model, train, return val_loss
|
||||
|
||||
# Synthetic loss surface for demonstration
|
||||
loss = (
|
||||
(lr - 0.01)**2 / 0.001 +
|
||||
(dropout - 0.15)**2 / 0.01 +
|
||||
(hidden_size - 128)**2 / 10000
|
||||
)
|
||||
return loss + np.random.normal(0, 0.001) # add noise
|
||||
|
||||
param_grid = {
|
||||
"lr": [0.001, 0.005, 0.01, 0.02, 0.05],
|
||||
"dropout": [0.0, 0.1, 0.2, 0.3],
|
||||
"hidden_size": [64, 128, 256],
|
||||
}
|
||||
|
||||
print(f"Total combinations: {5 * 4 * 3} = 60")
|
||||
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn=train_and_evaluate,
|
||||
param_grid=param_grid,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
print(f"\nBest configuration:")
|
||||
print(f" Learning rate: {best_params['lr']}")
|
||||
print(f" Dropout: {best_params['dropout']}")
|
||||
print(f" Hidden size: {best_params['hidden_size']}")
|
||||
print(f" Validation loss: {best_score:.4f}")
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Total combinations: 60
|
||||
[1/60] lr=0.001, dropout=0.0, hidden_size=64 → score=0.1892
|
||||
...
|
||||
[32/60] lr=0.01, dropout=0.2, hidden_size=128 → score=0.0027 [BEST]
|
||||
...
|
||||
|
||||
Best configuration:
|
||||
Learning rate: 0.01
|
||||
Dropout: 0.2
|
||||
Hidden size: 128
|
||||
Validation loss: 0.0027
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visualization
|
||||
|
||||
### Results Heatmap
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
# Collect all results
|
||||
results = {}
|
||||
for lr in param_grid["lr"]:
|
||||
for dropout in param_grid["dropout"]:
|
||||
params = {"lr": lr, "dropout": dropout, "hidden_size": 128}
|
||||
results[(lr, dropout)] = train_and_evaluate(params)
|
||||
|
||||
# Create heatmap
|
||||
lrs = param_grid["lr"]
|
||||
dropouts = param_grid["dropout"]
|
||||
Z = np.array([[results[(lr, d)] for d in dropouts] for lr in lrs])
|
||||
|
||||
plt.figure(figsize=(8, 6))
|
||||
plt.imshow(Z, origin='lower', aspect='auto', cmap='viridis_r')
|
||||
plt.colorbar(label='Loss')
|
||||
plt.xticks(range(len(dropouts)), dropouts)
|
||||
plt.yticks(range(len(lrs)), lrs)
|
||||
plt.xlabel('Dropout')
|
||||
plt.ylabel('Learning Rate')
|
||||
plt.title('Grid Search: Loss Surface')
|
||||
plt.savefig('grid_search_heatmap.png', dpi=150)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
Produces a 2D heatmap showing the loss landscape across different hyperparameter combinations:
|
||||
- **X-axis**: Dropout rate values
|
||||
- **Y-axis**: Learning rate values
|
||||
- **Color intensity**: Loss values (darker = lower loss = better performance)
|
||||
|
||||
This visualization helps identify optimal parameter regions and understand parameter interactions.
|
||||
|
||||
📓 **Real-World Examples**: See [04_real_world_applications.ipynb](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/04_real_world_applications.ipynb) for grid search applied to:
|
||||
- Neural network hyperparameter tuning
|
||||
- Portfolio optimization
|
||||
- Trading strategy parameter selection
|
||||
|
||||
---
|
||||
|
||||
## Combining with Other Methods
|
||||
|
||||
### Coarse-to-Fine Search
|
||||
|
||||
Use grid search to identify promising regions, then refine:
|
||||
|
||||
```python
|
||||
from optimizr import grid_search, differential_evolution
|
||||
|
||||
# Step 1: Coarse grid search
|
||||
coarse_grid = {
|
||||
"lr": [0.001, 0.01, 0.1],
|
||||
"dropout": [0.0, 0.2, 0.4],
|
||||
}
|
||||
|
||||
coarse_best, _ = grid_search(objective, coarse_grid)
|
||||
print(f"Coarse best: {coarse_best}")
|
||||
|
||||
# Step 2: Fine grid around best
|
||||
fine_grid = {
|
||||
"lr": np.linspace(
|
||||
coarse_best["lr"] * 0.5,
|
||||
coarse_best["lr"] * 2,
|
||||
10
|
||||
).tolist(),
|
||||
"dropout": np.linspace(
|
||||
max(0, coarse_best["dropout"] - 0.1),
|
||||
min(0.5, coarse_best["dropout"] + 0.1),
|
||||
10
|
||||
).tolist(),
|
||||
}
|
||||
|
||||
final_best, final_score = grid_search(objective, fine_grid)
|
||||
print(f"Final best: {final_best}, score: {final_score:.6f}")
|
||||
```
|
||||
|
||||
### Warm-Start Differential Evolution
|
||||
|
||||
Use grid search results to seed DE:
|
||||
|
||||
```python
|
||||
from optimizr import grid_search, differential_evolution
|
||||
|
||||
# Quick grid search for initial region
|
||||
best_grid, _ = grid_search(objective, coarse_grid)
|
||||
|
||||
# Initialize DE population around grid search result
|
||||
bounds = [
|
||||
(best_grid["lr"] * 0.1, best_grid["lr"] * 10),
|
||||
(max(0, best_grid["dropout"] - 0.2), min(0.5, best_grid["dropout"] + 0.2)),
|
||||
]
|
||||
|
||||
final_x, final_fx = differential_evolution(
|
||||
objective_fn=lambda x: objective({"lr": x[0], "dropout": x[1]}),
|
||||
bounds=bounds,
|
||||
maxiter=100,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Method | Evaluations | Coverage | Best For |
|
||||
|--------|-------------|----------|----------|
|
||||
| **Grid Search** | $\prod n_i$ (exponential) | Complete | Low-D, discrete |
|
||||
| Random Search | Fixed budget | Probabilistic | High-D, continuous |
|
||||
| Bayesian Optimization | Adaptive | Adaptive | Expensive objectives |
|
||||
| Differential Evolution | $N_P \times \text{gens}$ | Adaptive | Complex landscapes |
|
||||
|
||||
### When to Switch Methods
|
||||
|
||||
| Scenario | Recommendation |
|
||||
|----------|----------------|
|
||||
| D ≤ 3, fast objective | Grid search |
|
||||
| D = 4–10, moderate objective | Random search + grid refinement |
|
||||
| D > 10 or expensive objective | Bayesian optimization or DE |
|
||||
| Noisy objective | DE or ensemble methods |
|
||||
|
||||
---
|
||||
|
||||
## Advantages & Limitations
|
||||
|
||||
### Advantages
|
||||
|
||||
✅ **Simple and deterministic** — reproducible results
|
||||
|
||||
✅ **Complete coverage** — won't miss global optimum in grid
|
||||
|
||||
✅ **No tuning required** — no algorithm-specific hyperparameters
|
||||
|
||||
✅ **Parallel-friendly** — each evaluation is independent
|
||||
|
||||
✅ **Good for discrete parameters** — natural fit
|
||||
|
||||
### Limitations
|
||||
|
||||
❌ **Exponential scaling** — infeasible for many parameters
|
||||
|
||||
❌ **Wasteful for continuous spaces** — evaluates between optimal values
|
||||
|
||||
❌ **Uniform allocation** — doesn't focus on promising regions
|
||||
|
||||
❌ **Resolution trade-off** — coarse grids miss optima, fine grids explode
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
### 1. Start Coarse
|
||||
|
||||
Begin with 3–5 values per parameter. Refine after identifying good regions.
|
||||
|
||||
### 2. Use Log Scales
|
||||
|
||||
For parameters spanning orders of magnitude (learning rates, regularization):
|
||||
|
||||
```python
|
||||
lrs = np.logspace(-4, -1, 10) # 1e-4 to 0.1
|
||||
```
|
||||
|
||||
### 3. Limit Dimensions
|
||||
|
||||
Keep $D \leq 4$ for full grid search. Beyond that, use:
|
||||
- Random search (same budget, better coverage)
|
||||
- Successive halving
|
||||
- Bayesian optimization
|
||||
|
||||
### 4. Cache Expensive Computations
|
||||
|
||||
If objective shares preprocessing:
|
||||
|
||||
```python
|
||||
# Pre-compute shared data
|
||||
X_train, y_train = load_and_preprocess()
|
||||
|
||||
def objective(params):
|
||||
# Reuse X_train, y_train
|
||||
return train_model(X_train, y_train, **params)
|
||||
```
|
||||
|
||||
### 5. Save All Results
|
||||
|
||||
Store every evaluation for analysis:
|
||||
|
||||
```python
|
||||
all_results = []
|
||||
|
||||
def logging_objective(params):
|
||||
score = actual_objective(params)
|
||||
all_results.append({"params": params, "score": score})
|
||||
return score
|
||||
|
||||
grid_search(logging_objective, param_grid)
|
||||
|
||||
# Analyze all results afterward
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(all_results)
|
||||
print(df.sort_values("score").head(10))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Differential Evolution](differential_evolution.md) – Adaptive global optimization
|
||||
- [MCMC](mcmc.md) – Sampling-based exploration with uncertainty
|
||||
- [HMM](hmm.md) – Model selection with grid search over states
|
||||
@@ -0,0 +1,624 @@
|
||||
# Hidden Markov Models
|
||||
|
||||
Hidden Markov Models (HMMs) are probabilistic models for sequential data where observations
|
||||
are generated by a system that transitions between **hidden (latent) states**. Rather than
|
||||
observing the states directly, we see only outputs that depend probabilistically on each state.
|
||||
|
||||
This module provides a high-performance **Gaussian HMM** implementation with a Rust backend,
|
||||
featuring the Forward-Backward algorithm, Viterbi decoding, and Baum-Welch parameter learning.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### Model Definition
|
||||
|
||||
A Hidden Markov Model $\lambda$ is defined by three components:
|
||||
|
||||
#### 1. States
|
||||
|
||||
- **Number of states:** $N$
|
||||
- **State at time $t$:** $q_t \in \{1, 2, \ldots, N\}$
|
||||
- **State sequence:** $Q = q_1, q_2, \ldots, q_T$
|
||||
|
||||
#### 2. Observations
|
||||
|
||||
- **Observation at time $t$:** $o_t \in \mathbb{R}$ (continuous)
|
||||
- **Observation sequence:** $O = o_1, o_2, \ldots, o_T$
|
||||
|
||||
#### 3. Parameters
|
||||
|
||||
**Initial State Distribution:**
|
||||
|
||||
$$
|
||||
\pi_i = P(q_1 = i), \quad 1 \leq i \leq N
|
||||
$$
|
||||
|
||||
$$
|
||||
\sum_{i=1}^N \pi_i = 1
|
||||
$$
|
||||
|
||||
**State Transition Matrix:**
|
||||
|
||||
$$
|
||||
a_{ij} = P(q_{t+1} = j \mid q_t = i), \quad 1 \leq i,j \leq N
|
||||
$$
|
||||
|
||||
$$
|
||||
\sum_{j=1}^N a_{ij} = 1 \quad \text{for all } i
|
||||
$$
|
||||
|
||||
**Emission Distribution** (Gaussian):
|
||||
|
||||
$$
|
||||
b_j(o_t) = P(o_t \mid q_t = j) = \mathcal{N}(o_t; \mu_j, \sigma_j^2)
|
||||
$$
|
||||
|
||||
$$
|
||||
b_j(o_t) = \frac{1}{\sigma_j\sqrt{2\pi}} \exp\left(-\frac{(o_t - \mu_j)^2}{2\sigma_j^2}\right)
|
||||
$$
|
||||
|
||||
**Complete model:** $\lambda = (\boldsymbol{\pi}, \mathbf{A}, \mathbf{B})$
|
||||
|
||||
---
|
||||
|
||||
### The Markov Property
|
||||
|
||||
#### First-Order Markov Assumption
|
||||
|
||||
The future state depends only on the current state, not the history:
|
||||
|
||||
$$
|
||||
P(q_{t+1} \mid q_1, q_2, \ldots, q_t) = P(q_{t+1} \mid q_t)
|
||||
$$
|
||||
|
||||
#### Output Independence
|
||||
|
||||
Observations are conditionally independent given the state:
|
||||
|
||||
$$
|
||||
P(o_t \mid q_1, \ldots, q_T, o_1, \ldots, o_{t-1}, o_{t+1}, \ldots, o_T) = P(o_t \mid q_t)
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## The Three Fundamental Problems
|
||||
|
||||
HMMs are used to solve three fundamental problems:
|
||||
|
||||
| Problem | Given | Find | Solution |
|
||||
|---------|-------|------|----------|
|
||||
| **Evaluation** | Model $\lambda$, observations $O$ | $P(O \mid \lambda)$ | Forward Algorithm |
|
||||
| **Decoding** | Model $\lambda$, observations $O$ | Most likely state sequence $Q^*$ | Viterbi Algorithm |
|
||||
| **Learning** | Observations $O$ | Optimal parameters $\lambda^*$ | Baum-Welch Algorithm |
|
||||
|
||||
---
|
||||
|
||||
## Forward Algorithm
|
||||
|
||||
Computes $P(O \mid \lambda)$ efficiently using dynamic programming.
|
||||
|
||||
### Forward Variable
|
||||
|
||||
$$
|
||||
\alpha_t(i) = P(o_1, o_2, \ldots, o_t, q_t = i \mid \lambda)
|
||||
$$
|
||||
|
||||
The probability of observing the first $t$ observations AND being in state $i$ at time $t$.
|
||||
|
||||
### Algorithm
|
||||
|
||||
**Initialization** ($t = 1$):
|
||||
|
||||
$$
|
||||
\alpha_1(i) = \pi_i \cdot b_i(o_1), \quad 1 \leq i \leq N
|
||||
$$
|
||||
|
||||
**Recursion** ($1 \leq t < T$):
|
||||
|
||||
$$
|
||||
\alpha_{t+1}(j) = \left[\sum_{i=1}^N \alpha_t(i) \cdot a_{ij}\right] \cdot b_j(o_{t+1})
|
||||
$$
|
||||
|
||||
**Termination:**
|
||||
|
||||
$$
|
||||
P(O \mid \lambda) = \sum_{i=1}^N \alpha_T(i)
|
||||
$$
|
||||
|
||||
### Complexity
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Time | $O(N^2 T)$ |
|
||||
| Space | $O(N T)$ |
|
||||
| Without DP | $O(N^T)$ — exponential! |
|
||||
|
||||
---
|
||||
|
||||
## Backward Algorithm
|
||||
|
||||
Alternative computation for completeness and use in Baum-Welch.
|
||||
|
||||
### Backward Variable
|
||||
|
||||
$$
|
||||
\beta_t(i) = P(o_{t+1}, o_{t+2}, \ldots, o_T \mid q_t = i, \lambda)
|
||||
$$
|
||||
|
||||
**Initialization** ($t = T$):
|
||||
|
||||
$$
|
||||
\beta_T(i) = 1, \quad 1 \leq i \leq N
|
||||
$$
|
||||
|
||||
**Recursion** ($t = T-1, T-2, \ldots, 1$):
|
||||
|
||||
$$
|
||||
\beta_t(i) = \sum_{j=1}^N a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Viterbi Algorithm
|
||||
|
||||
Finds the single **most likely state sequence** given observations.
|
||||
|
||||
### Objective
|
||||
|
||||
$$
|
||||
Q^* = \arg\max_Q P(Q \mid O, \lambda) = \arg\max_Q P(Q, O \mid \lambda)
|
||||
$$
|
||||
|
||||
### Viterbi Variable
|
||||
|
||||
$$
|
||||
\delta_t(i) = \max_{q_1, \ldots, q_{t-1}} P(q_1, \ldots, q_{t-1}, q_t = i, o_1, \ldots, o_t \mid \lambda)
|
||||
$$
|
||||
|
||||
The maximum probability of any path ending in state $i$ at time $t$.
|
||||
|
||||
### Algorithm
|
||||
|
||||
**Initialization** ($t = 1$):
|
||||
|
||||
$$
|
||||
\delta_1(i) = \pi_i \cdot b_i(o_1)
|
||||
$$
|
||||
|
||||
$$
|
||||
\psi_1(i) = 0
|
||||
$$
|
||||
|
||||
**Recursion** ($2 \leq t \leq T$):
|
||||
|
||||
$$
|
||||
\delta_t(j) = \max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)
|
||||
$$
|
||||
|
||||
$$
|
||||
\psi_t(j) = \arg\max_{1 \leq i \leq N} \left[\delta_{t-1}(i) \cdot a_{ij}\right]
|
||||
$$
|
||||
|
||||
**Termination:**
|
||||
|
||||
$$
|
||||
P^* = \max_{1 \leq i \leq N} \delta_T(i)
|
||||
$$
|
||||
|
||||
$$
|
||||
q_T^* = \arg\max_{1 \leq i \leq N} \delta_T(i)
|
||||
$$
|
||||
|
||||
**Backtracking** ($t = T-1, T-2, \ldots, 1$):
|
||||
|
||||
$$
|
||||
q_t^* = \psi_{t+1}(q_{t+1}^*)
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Baum-Welch Algorithm
|
||||
|
||||
An **Expectation-Maximization (EM)** algorithm for learning HMM parameters from data.
|
||||
|
||||
### Auxiliary Variables
|
||||
|
||||
**State occupation probability:**
|
||||
|
||||
$$
|
||||
\gamma_t(i) = P(q_t = i \mid O, \lambda) = \frac{\alpha_t(i) \cdot \beta_t(i)}{\sum_{j=1}^N \alpha_t(j) \cdot \beta_t(j)}
|
||||
$$
|
||||
|
||||
**Transition probability:**
|
||||
|
||||
$$
|
||||
\xi_t(i,j) = P(q_t = i, q_{t+1} = j \mid O, \lambda)
|
||||
$$
|
||||
|
||||
$$
|
||||
= \frac{\alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}{\sum_{i=1}^N \sum_{j=1}^N \alpha_t(i) \cdot a_{ij} \cdot b_j(o_{t+1}) \cdot \beta_{t+1}(j)}
|
||||
$$
|
||||
|
||||
### E-Step
|
||||
|
||||
Compute $\gamma_t(i)$ and $\xi_t(i,j)$ for all $t$, $i$, $j$ using current parameters.
|
||||
|
||||
### M-Step
|
||||
|
||||
Update parameters to maximize expected log-likelihood:
|
||||
|
||||
**Initial state probabilities:**
|
||||
|
||||
$$
|
||||
\bar{\pi}_i = \gamma_1(i)
|
||||
$$
|
||||
|
||||
**Transition probabilities:**
|
||||
|
||||
$$
|
||||
\bar{a}_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i,j)}{\sum_{t=1}^{T-1} \gamma_t(i)}
|
||||
$$
|
||||
|
||||
**Emission parameters (Gaussian):**
|
||||
|
||||
$$
|
||||
\bar{\mu}_j = \frac{\sum_{t=1}^T \gamma_t(j) \cdot o_t}{\sum_{t=1}^T \gamma_t(j)}
|
||||
$$
|
||||
|
||||
$$
|
||||
\bar{\sigma}_j^2 = \frac{\sum_{t=1}^T \gamma_t(j) \cdot (o_t - \bar{\mu}_j)^2}{\sum_{t=1}^T \gamma_t(j)}
|
||||
$$
|
||||
|
||||
### Convergence
|
||||
|
||||
Iterate E-step and M-step until:
|
||||
|
||||
$$
|
||||
|L(\lambda^{(k+1)}) - L(\lambda^{(k)})| < \epsilon
|
||||
$$
|
||||
|
||||
where $L(\lambda) = \log P(O \mid \lambda)$ is the log-likelihood.
|
||||
|
||||
**Properties:**
|
||||
- Guaranteed to converge to a **local maximum**
|
||||
- May converge to different solutions depending on initialization
|
||||
- Multiple random restarts recommended
|
||||
|
||||
---
|
||||
|
||||
## Numerical Stability
|
||||
|
||||
### Scaling
|
||||
|
||||
Raw probabilities underflow for long sequences. Use **scaling factors**:
|
||||
|
||||
$$
|
||||
c_t = \frac{1}{\sum_{i=1}^N \alpha_t(i)}
|
||||
$$
|
||||
|
||||
Scaled forward variables:
|
||||
|
||||
$$
|
||||
\hat{\alpha}_t(i) = c_t \cdot \alpha_t(i)
|
||||
$$
|
||||
|
||||
### Log-Space Computation
|
||||
|
||||
For Viterbi, work in log-space:
|
||||
|
||||
$$
|
||||
\log \delta_t(j) = \max_{1 \leq i \leq N} \left[\log \delta_{t-1}(i) + \log a_{ij}\right] + \log b_j(o_t)
|
||||
$$
|
||||
|
||||
Use log-sum-exp for stable additions:
|
||||
|
||||
$$
|
||||
\log(e^a + e^b) = \max(a,b) + \log(1 + e^{-|a-b|})
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Model Selection
|
||||
|
||||
### Number of States
|
||||
|
||||
Choose the number of states $N$ using information criteria:
|
||||
|
||||
| Criterion | Formula | Description |
|
||||
|-----------|---------|-------------|
|
||||
| **AIC** | $-2\log L + 2k$ | Akaike Information Criterion |
|
||||
| **BIC** | $-2\log L + k\log n$ | Bayesian Information Criterion |
|
||||
|
||||
where $k$ is the number of parameters and $n$ is the sample size.
|
||||
|
||||
**Lower values indicate better models** (penalized for complexity).
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import HMM
|
||||
|
||||
# Generate synthetic regime data
|
||||
np.random.seed(42)
|
||||
returns = np.concatenate([
|
||||
np.random.normal(0.01, 0.02, 500), # Bull regime
|
||||
np.random.normal(-0.015, 0.03, 500), # Bear regime
|
||||
])
|
||||
|
||||
# Fit a 2-state HMM
|
||||
model = HMM(n_states=2)
|
||||
model.fit(returns, n_iterations=100)
|
||||
|
||||
# Decode the most likely state sequence
|
||||
states = model.predict(returns)
|
||||
print("State counts:", np.unique(states, return_counts=True))
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
State counts: (array([0, 1]), array([498, 502]))
|
||||
```
|
||||
|
||||
### Model Inspection
|
||||
|
||||
```python
|
||||
# Examine learned parameters
|
||||
print("Initial distribution:", model.pi)
|
||||
print("Transition matrix:\n", model.A)
|
||||
print("Emission means:", model.means)
|
||||
print("Emission stds:", model.stds)
|
||||
|
||||
# Compute log-likelihood
|
||||
ll = model.score(returns)
|
||||
print(f"Log-likelihood: {ll:.2f}")
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Initial distribution: [0.95 0.05]
|
||||
Transition matrix:
|
||||
[[0.992 0.008]
|
||||
[0.010 0.990]]
|
||||
Emission means: [ 0.0098 -0.0152]
|
||||
Emission stds: [0.0198 0.0301]
|
||||
Log-likelihood: 2847.31
|
||||
```
|
||||
|
||||
### Feature Engineering for HMM
|
||||
|
||||
```python
|
||||
from optimizr import prepare_for_hmm_py
|
||||
|
||||
# Prepare features from price data with rolling statistics
|
||||
features = prepare_for_hmm_py(
|
||||
prices,
|
||||
lag_periods=[1, 5, 20], # multi-scale lookback
|
||||
)
|
||||
|
||||
# Fit HMM with richer features
|
||||
hmm = HMM(n_states=3).fit(features, n_iterations=120)
|
||||
```
|
||||
|
||||
Use rolling statistics from `timeseries_utils` as additional features for richer
|
||||
regime classification:
|
||||
|
||||
```python
|
||||
from optimizr import rolling_hurst, rolling_halflife
|
||||
|
||||
hurst = rolling_hurst(prices, window=50)
|
||||
halflife = rolling_halflife(prices, window=50)
|
||||
features = np.column_stack([returns, hurst, halflife])
|
||||
|
||||
hmm = HMM(n_states=3).fit(features, n_iterations=100)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visualization
|
||||
|
||||
### State Sequence Plot
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, axes = plt.subplots(2, 1, figsize=(12, 6), sharex=True)
|
||||
|
||||
# Price/returns
|
||||
axes[0].plot(returns, alpha=0.7)
|
||||
axes[0].set_ylabel('Returns')
|
||||
axes[0].set_title('Returns Time Series')
|
||||
|
||||
# State sequence
|
||||
axes[1].plot(states, drawstyle='steps-post', color='orange')
|
||||
axes[1].set_ylabel('State')
|
||||
axes[1].set_xlabel('Time')
|
||||
axes[1].set_title('Decoded State Sequence')
|
||||
axes[1].set_yticks([0, 1])
|
||||
axes[1].set_yticklabels(['Bull', 'Bear'])
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('hmm_states.png', dpi=150)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
This visualization creates a two-panel plot:
|
||||
- **Top panel**: Returns time series showing price movements
|
||||
- **Bottom panel**: Decoded state sequence highlighting regime transitions (e.g., Bull/Bear markets)
|
||||
|
||||
For complete examples with regime detection on real market data, see the [HMM Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/01_hmm_tutorial.ipynb).
|
||||
|
||||
### Transition Diagram
|
||||
|
||||
```{mermaid}
|
||||
stateDiagram-v2
|
||||
direction LR
|
||||
[*] --> Normal
|
||||
Bull : 📈 Bull Market
|
||||
Normal : 📊 Normal Regime
|
||||
Bear : 📉 Bear Market
|
||||
Bull --> Bull : a₁₁ (self)
|
||||
Bull --> Normal : a₁₂
|
||||
Normal --> Bull : a₂₁
|
||||
Normal --> Normal : a₂₂ (self)
|
||||
Normal --> Bear : a₂₃
|
||||
Bear --> Normal : a₃₂
|
||||
Bear --> Bear : a₃₃ (self)
|
||||
```
|
||||
|
||||
**Code example** — build and visualize the transition graph programmatically:
|
||||
|
||||
```python
|
||||
import networkx as nx
|
||||
|
||||
G = nx.DiGraph()
|
||||
for i in range(model.n_states):
|
||||
for j in range(model.n_states):
|
||||
if model.A[i, j] > 0.01: # threshold small probabilities
|
||||
G.add_edge(f"State {i}", f"State {j}", weight=model.A[i, j])
|
||||
|
||||
pos = nx.spring_layout(G)
|
||||
edge_labels = {(u, v): f"{d['weight']:.2f}" for u, v, d in G.edges(data=True)}
|
||||
|
||||
plt.figure(figsize=(8, 6))
|
||||
nx.draw(G, pos, with_labels=True, node_size=2000, node_color='lightblue')
|
||||
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
|
||||
plt.title('HMM Transition Diagram')
|
||||
plt.savefig('hmm_transitions.png', dpi=150)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
Generates a directed graph visualization showing:
|
||||
- **Nodes**: Hidden states (e.g., State 0, State 1)
|
||||
- **Edges**: Transition probabilities between states (labeled with probability values)
|
||||
- Only transitions with probability > 0.01 are shown for clarity
|
||||
|
||||
📓 **Full Tutorial**: Explore [01_hmm_tutorial.ipynb](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/01_hmm_tutorial.ipynb) for hands-on examples including:
|
||||
- Multi-state regime detection
|
||||
- Volatility clustering analysis
|
||||
- Mean-reversion regime identification
|
||||
- Viterbi decoding in financial time series
|
||||
|
||||
---
|
||||
|
||||
## Applications
|
||||
|
||||
### 1. Financial Regime Detection
|
||||
|
||||
HMMs identify market regimes (bull/bear/sideways) from returns:
|
||||
|
||||
```python
|
||||
from optimizr import HMM
|
||||
|
||||
# Daily returns
|
||||
hmm = HMM(n_states=3) # bull, bear, high-volatility
|
||||
hmm.fit(daily_returns)
|
||||
|
||||
regimes = hmm.predict(daily_returns)
|
||||
regime_labels = ['Bull', 'Bear', 'High Vol']
|
||||
current_regime = regime_labels[regimes[-1]]
|
||||
print(f"Current market regime: {current_regime}")
|
||||
```
|
||||
|
||||
### 2. Mean Reversion Trading
|
||||
|
||||
Use regime as a filter for mean-reversion strategies:
|
||||
|
||||
```python
|
||||
# Only trade mean-reversion when in low-volatility regime
|
||||
regime = hmm.predict(recent_returns)[-1]
|
||||
if regime == 0: # low-vol regime
|
||||
# Execute mean-reversion strategy
|
||||
pass
|
||||
else:
|
||||
# Hold or reduce position
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Risk Management
|
||||
|
||||
Adjust position sizing based on detected regime:
|
||||
|
||||
```python
|
||||
regime_volatilities = {
|
||||
0: 0.02, # low vol
|
||||
1: 0.03, # medium vol
|
||||
2: 0.05, # high vol
|
||||
}
|
||||
|
||||
current_regime = hmm.predict(returns)[-1]
|
||||
position_size = base_size * (target_vol / regime_volatilities[current_regime])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks on Apple M1 (1000 observations):
|
||||
|
||||
| States | Fit Time | Predict Time | Memory |
|
||||
|--------|----------|--------------|--------|
|
||||
| 2 | 45 ms | 2 ms | 0.5 MB |
|
||||
| 3 | 68 ms | 3 ms | 0.8 MB |
|
||||
| 5 | 124 ms | 6 ms | 1.4 MB |
|
||||
| 10 | 412 ms | 18 ms | 4.2 MB |
|
||||
|
||||
**Complexity:** $O(N^2 T)$ for both forward-backward and Viterbi.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| States not separating | Too few iterations | Increase `n_iterations` to 150–200 |
|
||||
| Converges to bad solution | Local optimum | Run multiple restarts with different seeds |
|
||||
| Underflow errors | Long sequences | Enable log-space computation (automatic in Rust backend) |
|
||||
| All observations in one state | Poor initialization | Try more states or normalize input data |
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- **Normalize inputs:** Scale features to similar ranges for better learning.
|
||||
- **Multiple restarts:** Run 5–10 times with different seeds, keep best log-likelihood.
|
||||
- **Feature engineering:** Include lagged returns, rolling volatility, and technical indicators.
|
||||
- **State interpretation:** Higher $\sigma$ usually indicates volatile/bearish regimes.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Uses Rust backend when available; falls back to Python if unavailable.
|
||||
- `fit` runs Baum-Welch; `predict` runs Viterbi.
|
||||
- Call `score(X)` to compute log-likelihood for model comparison.
|
||||
- Scaling is applied automatically to prevent numerical underflow.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Rabiner, L.R. (1989). "A tutorial on hidden Markov models and selected applications in speech recognition." *Proceedings of the IEEE*, 77(2):257–286.
|
||||
|
||||
2. Baum, L.E. & Petrie, T. (1966). "Statistical inference for probabilistic functions of finite state Markov chains." *Annals of Mathematical Statistics*, 37(6):1554–1563.
|
||||
|
||||
3. Viterbi, A. (1967). "Error bounds for convolutional codes and an asymptotically optimum decoding algorithm." *IEEE Trans. Info. Theory*, 13(2):260–269.
|
||||
|
||||
4. Hamilton, J.D. (1989). "A new approach to the economic analysis of nonstationary time series and the business cycle." *Econometrica*, 57(2):357–384.
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [MCMC](mcmc.md) – Alternative inference for more complex latent variable models
|
||||
- [Differential Evolution](differential_evolution.md) – Global optimization for HMM initialization
|
||||
- [Mean Field Games](mean_field_games.md) – Population dynamics with strategic interactions
|
||||
@@ -0,0 +1,60 @@
|
||||
Matrix Riccati Solver
|
||||
=====================
|
||||
|
||||
The module :code:`optimal_control::matrix_riccati` integrates backward in time
|
||||
the matrix Riccati differential equation
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{dA(t)}{dt} = -2\,A(t)\,M\,A(t) + Q,
|
||||
\qquad A(T) = A_T,
|
||||
|
||||
together with the affine and constant components
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{dB(t)}{dt} = -2\,A(t)\,M\,B(t),
|
||||
\qquad B(T) = B_T,
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{dC(t)}{dt} = -B(t)^\top\,M\,B(t),
|
||||
\qquad C(T) = C_T.
|
||||
|
||||
Discretisation
|
||||
--------------
|
||||
|
||||
The grid :math:`\{t_n = T - n\,\Delta t\}_{n=0}^{N}` with
|
||||
:math:`\Delta t = T / N` is traversed backward and a classical RK4 step is
|
||||
applied to the joint vector field :math:`(A, B, C)`. Each macro step is
|
||||
optionally subdivided into :math:`s` sub-steps for stability on stiff
|
||||
problems.
|
||||
|
||||
Validation
|
||||
----------
|
||||
|
||||
In the scalar case :math:`A, M, Q \in \mathbb{R}` with :math:`A(T) = 0`,
|
||||
|
||||
.. math::
|
||||
|
||||
A(t) \;=\; -\sqrt{\frac{Q}{2M}}\;\tanh\!\Big(\sqrt{2QM}\,(T - t)\Big),
|
||||
|
||||
a closed form used by the unit test :code:`scalar_riccati_matches_analytic`
|
||||
to certify :math:`L^\infty` convergence below :math:`10^{-5}` on
|
||||
:math:`[0, T]`.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub fn solve_matrix_riccati(
|
||||
m_matrix: ArrayView2<f64>,
|
||||
q: ArrayView2<f64>,
|
||||
n: ArrayView2<f64>,
|
||||
a_terminal: ArrayView2<f64>,
|
||||
b_terminal: ArrayView1<f64>,
|
||||
c_terminal: f64,
|
||||
t_horizon: f64,
|
||||
config: RiccatiConfig,
|
||||
) -> Result<RiccatiResult>;
|
||||
@@ -0,0 +1,578 @@
|
||||
# MCMC Sampling
|
||||
|
||||
**Markov Chain Monte Carlo (MCMC)** methods are a class of algorithms for sampling from
|
||||
probability distributions by constructing a Markov chain whose stationary distribution
|
||||
equals the target distribution. MCMC is fundamental to Bayesian inference, computational
|
||||
statistics, and quantitative finance.
|
||||
|
||||
This module provides a high-performance **Metropolis-Hastings sampler** with Rust
|
||||
acceleration, designed for Bayesian parameter estimation and posterior exploration.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### The Monte Carlo Goal
|
||||
|
||||
Sample from a target distribution $\pi(\theta)$ where:
|
||||
|
||||
- Direct sampling is difficult or impossible
|
||||
- We can evaluate $\pi(\theta)$ **up to a normalization constant**
|
||||
|
||||
Given samples $\theta^{(1)}, \ldots, \theta^{(N)} \sim \pi(\theta)$, we approximate:
|
||||
|
||||
**Expectations:**
|
||||
|
||||
$$
|
||||
\mathbb{E}_\pi[f(\theta)] \approx \frac{1}{N}\sum_{i=1}^N f(\theta^{(i)})
|
||||
$$
|
||||
|
||||
**Probabilities:**
|
||||
|
||||
$$
|
||||
P(\theta \in A) \approx \frac{1}{N}\sum_{i=1}^N \mathbb{1}[\theta^{(i)} \in A]
|
||||
$$
|
||||
|
||||
**Quantiles**, **posterior intervals**, and other distributional properties.
|
||||
|
||||
---
|
||||
|
||||
### Markov Chains
|
||||
|
||||
A sequence $\theta^{(0)}, \theta^{(1)}, \theta^{(2)}, \ldots$ is a **Markov chain** if:
|
||||
|
||||
$$
|
||||
P(\theta^{(t+1)} \mid \theta^{(0)}, \ldots, \theta^{(t)}) = P(\theta^{(t+1)} \mid \theta^{(t)})
|
||||
$$
|
||||
|
||||
The next state depends only on the current state.
|
||||
|
||||
### Transition Kernel
|
||||
|
||||
$$
|
||||
K(\theta' \mid \theta) = P(\theta^{(t+1)} = \theta' \mid \theta^{(t)} = \theta)
|
||||
$$
|
||||
|
||||
### Stationary Distribution
|
||||
|
||||
A distribution $\pi(\theta)$ is **stationary** if:
|
||||
|
||||
$$
|
||||
\pi(\theta') = \int K(\theta' \mid \theta) \, \pi(\theta) \, d\theta
|
||||
$$
|
||||
|
||||
If we start with $\theta^{(0)} \sim \pi$, then $\theta^{(t)} \sim \pi$ for all $t$.
|
||||
|
||||
### Ergodicity
|
||||
|
||||
A Markov chain is **ergodic** if:
|
||||
|
||||
1. **Irreducible:** Can reach any state from any state
|
||||
2. **Aperiodic:** No cyclic behavior
|
||||
|
||||
For ergodic chains with stationary distribution $\pi$:
|
||||
|
||||
$$
|
||||
\lim_{t \to \infty} P(\theta^{(t)} \in A) = \pi(A)
|
||||
$$
|
||||
|
||||
regardless of initial state $\theta^{(0)}$.
|
||||
|
||||
### Detailed Balance
|
||||
|
||||
A sufficient condition for $\pi$ to be stationary:
|
||||
|
||||
$$
|
||||
\pi(\theta) \, K(\theta' \mid \theta) = \pi(\theta') \, K(\theta \mid \theta')
|
||||
$$
|
||||
|
||||
**Reversibility:** The probability of going $\theta \to \theta'$ equals that of $\theta' \to \theta$.
|
||||
|
||||
---
|
||||
|
||||
## Metropolis-Hastings Algorithm
|
||||
|
||||
The MH algorithm constructs a Markov chain whose stationary distribution is the target $\pi(\theta)$.
|
||||
|
||||
### Algorithm
|
||||
|
||||
**Input:** Target distribution $\pi(\theta)$, proposal distribution $q(\theta' \mid \theta)$
|
||||
|
||||
```
|
||||
Algorithm: Metropolis-Hastings
|
||||
──────────────────────────────
|
||||
1. Initialize θ⁽⁰⁾
|
||||
|
||||
2. For t = 0, 1, 2, ..., N-1:
|
||||
|
||||
a. Propose: Draw θ* ~ q(θ* | θ⁽ᵗ⁾)
|
||||
|
||||
b. Compute acceptance probability:
|
||||
α = min(1, [π(θ*) · q(θ⁽ᵗ⁾|θ*)] / [π(θ⁽ᵗ⁾) · q(θ*|θ⁽ᵗ⁾)])
|
||||
|
||||
c. Accept or reject:
|
||||
u ~ Uniform(0, 1)
|
||||
if u < α:
|
||||
θ⁽ᵗ⁺¹⁾ = θ* # accept
|
||||
else:
|
||||
θ⁽ᵗ⁺¹⁾ = θ⁽ᵗ⁾ # reject
|
||||
|
||||
3. Return samples {θ⁽¹⁾, θ⁽²⁾, ..., θ⁽ᴺ⁾}
|
||||
```
|
||||
|
||||
### Acceptance Probability
|
||||
|
||||
$$
|
||||
\alpha = \min\left(1, \frac{\pi(\theta^*) \, q(\theta^{(t)} \mid \theta^*)}{\pi(\theta^{(t)}) \, q(\theta^* \mid \theta^{(t)})}\right)
|
||||
$$
|
||||
|
||||
The ratio $\pi(\theta^*)/\pi(\theta^{(t)})$ compares likelihoods. The ratio
|
||||
$q(\theta^{(t)} \mid \theta^*)/q(\theta^* \mid \theta^{(t)})$ corrects for asymmetric proposals.
|
||||
|
||||
### Why It Works
|
||||
|
||||
**Theorem:** The MH algorithm produces a Markov chain with stationary distribution $\pi(\theta)$.
|
||||
|
||||
The acceptance rule ensures **detailed balance** holds, guaranteeing convergence to $\pi$.
|
||||
|
||||
---
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Metropolis Algorithm (Symmetric Proposal)
|
||||
|
||||
When the proposal is **symmetric:** $q(\theta' \mid \theta) = q(\theta \mid \theta')$
|
||||
|
||||
Acceptance probability simplifies to:
|
||||
|
||||
$$
|
||||
\alpha = \min\left(1, \frac{\pi(\theta^*)}{\pi(\theta^{(t)})}\right)
|
||||
$$
|
||||
|
||||
Always accept moves to higher probability; sometimes accept moves to lower probability.
|
||||
|
||||
### Random Walk Metropolis
|
||||
|
||||
Use a Gaussian proposal centered at the current state:
|
||||
|
||||
$$
|
||||
q(\theta' \mid \theta) = \mathcal{N}(\theta' \mid \theta, \sigma^2 \mathbf{I})
|
||||
$$
|
||||
|
||||
This is symmetric, so Metropolis acceptance applies.
|
||||
|
||||
**This is what Optimiz-rs implements.**
|
||||
|
||||
---
|
||||
|
||||
## Bayesian Inference with MCMC
|
||||
|
||||
### Bayes' Theorem
|
||||
|
||||
$$
|
||||
p(\theta \mid D) = \frac{p(D \mid \theta) \, p(\theta)}{p(D)}
|
||||
$$
|
||||
|
||||
| Term | Name | Description |
|
||||
|------|------|-------------|
|
||||
| $p(\theta \mid D)$ | Posterior | What we want |
|
||||
| $p(D \mid \theta)$ | Likelihood | How well parameters explain data |
|
||||
| $p(\theta)$ | Prior | Beliefs before seeing data |
|
||||
| $p(D)$ | Evidence | Normalizing constant (often intractable) |
|
||||
|
||||
### MCMC for Posterior Sampling
|
||||
|
||||
The evidence $p(D)$ is often intractable, but we can evaluate:
|
||||
|
||||
$$
|
||||
\pi(\theta) \propto p(D \mid \theta) \cdot p(\theta)
|
||||
$$
|
||||
|
||||
MCMC only needs $\pi$ **up to a constant**, so we can sample from the posterior!
|
||||
|
||||
### Log-Posterior
|
||||
|
||||
In practice, work with log-probabilities to avoid underflow:
|
||||
|
||||
$$
|
||||
\log \pi(\theta) = \log p(D \mid \theta) + \log p(\theta) + \text{const}
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import mcmc_sample
|
||||
|
||||
# Define log-likelihood for a Gaussian model
|
||||
def log_likelihood(params, data):
|
||||
mu, sigma = params
|
||||
if sigma <= 0:
|
||||
return -np.inf # invalid parameter
|
||||
residuals = (data - mu) / sigma
|
||||
return -0.5 * np.sum(residuals**2) - len(data) * np.log(sigma)
|
||||
|
||||
# Generate synthetic data: N(1.2, 1.0)
|
||||
np.random.seed(42)
|
||||
observations = np.random.randn(1000) + 1.2
|
||||
|
||||
# Run MCMC sampling
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_likelihood,
|
||||
data=observations,
|
||||
initial_params=np.array([0.0, 1.0]),
|
||||
param_bounds=[(-5, 5), (0.1, 5.0)],
|
||||
n_samples=8000,
|
||||
burn_in=500,
|
||||
proposal_std=0.2,
|
||||
)
|
||||
|
||||
print("Posterior mean:", samples.mean(axis=0))
|
||||
print("Posterior std:", samples.std(axis=0))
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Posterior mean: [1.198 0.987]
|
||||
Posterior std: [0.032 0.022]
|
||||
```
|
||||
|
||||
The true values (1.2, 1.0) are recovered within posterior uncertainty.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```python
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_likelihood,
|
||||
data=observations,
|
||||
initial_params=np.array([0.0, 1.0]),
|
||||
param_bounds=[(-5, 5), (0.1, 5.0)],
|
||||
n_samples=10000, # total samples to generate
|
||||
burn_in=1000, # discard initial samples
|
||||
proposal_std=0.15, # step size for random walk
|
||||
thin=2, # keep every 2nd sample
|
||||
seed=42, # for reproducibility
|
||||
)
|
||||
```
|
||||
|
||||
### Posterior Analysis
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Trace plots
|
||||
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
|
||||
|
||||
# Mu trace
|
||||
axes[0, 0].plot(samples[:, 0], alpha=0.7)
|
||||
axes[0, 0].set_ylabel('μ')
|
||||
axes[0, 0].set_title('Trace: μ')
|
||||
axes[0, 0].axhline(1.2, color='r', linestyle='--', label='True')
|
||||
|
||||
# Sigma trace
|
||||
axes[0, 1].plot(samples[:, 1], alpha=0.7)
|
||||
axes[0, 1].set_ylabel('σ')
|
||||
axes[0, 1].set_title('Trace: σ')
|
||||
axes[0, 1].axhline(1.0, color='r', linestyle='--', label='True')
|
||||
|
||||
# Mu histogram
|
||||
axes[1, 0].hist(samples[:, 0], bins=50, density=True, alpha=0.7)
|
||||
axes[1, 0].axvline(1.2, color='r', linestyle='--', label='True')
|
||||
axes[1, 0].set_xlabel('μ')
|
||||
axes[1, 0].set_title('Posterior: μ')
|
||||
|
||||
# Sigma histogram
|
||||
axes[1, 1].hist(samples[:, 1], bins=50, density=True, alpha=0.7)
|
||||
axes[1, 1].axvline(1.0, color='r', linestyle='--', label='True')
|
||||
axes[1, 1].set_xlabel('σ')
|
||||
axes[1, 1].set_title('Posterior: σ')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('mcmc_posterior.png', dpi=150)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Convergence Diagnostics
|
||||
|
||||
### Burn-in Period
|
||||
|
||||
Discard initial samples before the chain has converged to the stationary distribution.
|
||||
|
||||
**How to choose:**
|
||||
|
||||
- Plot trace plots and look for stabilization
|
||||
- Typically 1000–10000 iterations
|
||||
- Conservative: discard first 50% of samples
|
||||
|
||||
### Effective Sample Size (ESS)
|
||||
|
||||
Due to autocorrelation, MCMC samples are not independent:
|
||||
|
||||
$$
|
||||
\text{ESS} = \frac{N}{1 + 2\sum_{k=1}^\infty \rho_k}
|
||||
$$
|
||||
|
||||
where $\rho_k$ is the autocorrelation at lag $k$.
|
||||
|
||||
**Interpretation:** ESS ≈ number of independent samples.
|
||||
|
||||
**Goal:** ESS > 400 for reliable posterior estimates.
|
||||
|
||||
### Autocorrelation
|
||||
|
||||
$$
|
||||
\rho_k = \frac{\text{Cov}(\theta^{(t)}, \theta^{(t+k)})}{\text{Var}(\theta^{(t)})}
|
||||
$$
|
||||
|
||||
| Autocorrelation | Interpretation |
|
||||
|-----------------|----------------|
|
||||
| Low (< 0.1) | Fast mixing, efficient sampling |
|
||||
| High (> 0.5) | Slow mixing, need more samples or better tuning |
|
||||
|
||||
### Gelman-Rubin Diagnostic ($\hat{R}$)
|
||||
|
||||
Run multiple chains with different starting points:
|
||||
|
||||
$$
|
||||
\hat{R} = \sqrt{\frac{\text{Var}^+}{\text{Within-chain variance}}}
|
||||
$$
|
||||
|
||||
| $\hat{R}$ Value | Interpretation |
|
||||
|-----------------|----------------|
|
||||
| ≈ 1.0 | Chains have converged |
|
||||
| > 1.1 | Chains have NOT mixed — run longer |
|
||||
|
||||
---
|
||||
|
||||
## Proposal Tuning
|
||||
|
||||
### Acceptance Rate
|
||||
|
||||
**Optimal acceptance rate** (for random walk Metropolis):
|
||||
|
||||
| Dimension | Optimal Rate |
|
||||
|-----------|--------------|
|
||||
| 1D | 44% |
|
||||
| High-D | 23.4% |
|
||||
| Practical | 20–40% |
|
||||
|
||||
**Tuning guidance:**
|
||||
|
||||
| Acceptance Rate | Problem | Fix |
|
||||
|-----------------|---------|-----|
|
||||
| Too high (> 50%) | Proposals too small | Increase `proposal_std` |
|
||||
| Too low (< 10%) | Proposals too large | Decrease `proposal_std` |
|
||||
|
||||
### Adaptive Tuning
|
||||
|
||||
During burn-in, automatically adjust proposal variance:
|
||||
|
||||
```python
|
||||
# Start with initial guess, let Rust backend tune
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_likelihood,
|
||||
data=observations,
|
||||
initial_params=initial,
|
||||
param_bounds=bounds,
|
||||
n_samples=10000,
|
||||
burn_in=2000, # longer burn-in for adaptation
|
||||
proposal_std=0.5, # initial value, will be adjusted
|
||||
adaptive=True, # enable adaptive tuning
|
||||
)
|
||||
```
|
||||
|
||||
### Optimal Scaling
|
||||
|
||||
Roberts and Rosenthal (2001): For Gaussian targets in $d$ dimensions:
|
||||
|
||||
$$
|
||||
\sigma^2_{\text{optimal}} = \frac{2.38^2}{d} \cdot \Sigma
|
||||
$$
|
||||
|
||||
where $\Sigma$ is the posterior covariance.
|
||||
|
||||
---
|
||||
|
||||
## Applications
|
||||
|
||||
### 1. Bayesian Regression
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import mcmc_sample
|
||||
|
||||
def log_posterior(params, data):
|
||||
X, y = data
|
||||
beta = params[:-1]
|
||||
sigma = params[-1]
|
||||
|
||||
if sigma <= 0:
|
||||
return -np.inf
|
||||
|
||||
# Likelihood
|
||||
y_pred = X @ beta
|
||||
residuals = (y - y_pred) / sigma
|
||||
ll = -0.5 * np.sum(residuals**2) - len(y) * np.log(sigma)
|
||||
|
||||
# Prior: N(0, 10) for beta, InvGamma for sigma
|
||||
log_prior = -0.5 * np.sum(beta**2) / 100
|
||||
|
||||
return ll + log_prior
|
||||
|
||||
# Fit Bayesian linear regression
|
||||
X = np.column_stack([np.ones(100), np.random.randn(100)])
|
||||
y = 2 + 3 * X[:, 1] + np.random.randn(100) * 0.5
|
||||
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_posterior,
|
||||
data=(X, y),
|
||||
initial_params=np.array([0.0, 0.0, 1.0]),
|
||||
param_bounds=[(-10, 10), (-10, 10), (0.01, 5)],
|
||||
n_samples=5000,
|
||||
burn_in=500,
|
||||
)
|
||||
|
||||
print("Intercept:", samples[:, 0].mean(), "±", samples[:, 0].std())
|
||||
print("Slope:", samples[:, 1].mean(), "±", samples[:, 1].std())
|
||||
print("Sigma:", samples[:, 2].mean(), "±", samples[:, 2].std())
|
||||
```
|
||||
|
||||
### 2. Stochastic Volatility
|
||||
|
||||
```python
|
||||
def log_posterior_sv(params, returns):
|
||||
mu, phi, sigma_v = params
|
||||
|
||||
if not (0 < phi < 1) or sigma_v <= 0:
|
||||
return -np.inf
|
||||
|
||||
# Autoregressive volatility model
|
||||
T = len(returns)
|
||||
log_var = np.zeros(T)
|
||||
log_var[0] = mu / (1 - phi)
|
||||
|
||||
for t in range(1, T):
|
||||
log_var[t] = mu + phi * (log_var[t-1] - mu)
|
||||
|
||||
# Likelihood
|
||||
ll = -0.5 * np.sum(returns**2 / np.exp(log_var) + log_var)
|
||||
|
||||
return ll
|
||||
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_posterior_sv,
|
||||
data=daily_returns,
|
||||
initial_params=np.array([-1.0, 0.9, 0.2]),
|
||||
param_bounds=[(-5, 0), (0.01, 0.99), (0.01, 1.0)],
|
||||
n_samples=10000,
|
||||
burn_in=2000,
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Portfolio Optimization with Uncertainty
|
||||
|
||||
```python
|
||||
# Sample from posterior of expected returns
|
||||
posterior_means = samples[:, :n_assets]
|
||||
|
||||
# For each posterior sample, compute optimal weights
|
||||
optimal_weights = []
|
||||
for mu_sample in posterior_means[::10]: # thin for speed
|
||||
w = optimize_portfolio(mu_sample, cov_matrix)
|
||||
optimal_weights.append(w)
|
||||
|
||||
# Report posterior distribution of weights
|
||||
weights_mean = np.mean(optimal_weights, axis=0)
|
||||
weights_std = np.std(optimal_weights, axis=0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks on Apple M1:
|
||||
|
||||
| Parameters | Samples | Time | Samples/sec |
|
||||
|------------|---------|------|-------------|
|
||||
| 2 | 10,000 | 0.8 s | 12,500 |
|
||||
| 5 | 10,000 | 1.2 s | 8,333 |
|
||||
| 10 | 10,000 | 2.1 s | 4,762 |
|
||||
| 20 | 10,000 | 4.8 s | 2,083 |
|
||||
|
||||
Performance scales approximately linearly with the number of parameters.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Acceptance rate ~0% | `proposal_std` too large | Decrease by 50% |
|
||||
| Acceptance rate ~100% | `proposal_std` too small | Increase by 50–100% |
|
||||
| Chains stuck | Local mode | Use multiple chains, different starts |
|
||||
| Poor mixing | Strong correlations | Reparameterize or increase samples |
|
||||
| `log_likelihood` returns `-inf` | Invalid parameters | Check bounds, add guards |
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
### 1. Keep `proposal_std` Modest
|
||||
|
||||
Start with 0.1–0.5 of the expected posterior standard deviation. Adjust to
|
||||
achieve 20–40% acceptance rate.
|
||||
|
||||
### 2. Use Adequate Burn-in
|
||||
|
||||
`burn_in` should be at least 5–10% of total samples for stable chains.
|
||||
|
||||
### 3. Provide Tight Bounds
|
||||
|
||||
Specify `param_bounds` to avoid exploring invalid regions (negative variances, etc.).
|
||||
|
||||
### 4. Monitor Convergence
|
||||
|
||||
Always check trace plots and autocorrelation before using posterior samples.
|
||||
|
||||
### 5. Multiple Chains
|
||||
|
||||
Run 2–4 chains from different starting points. Compare posteriors and compute $\hat{R}$.
|
||||
|
||||
---
|
||||
|
||||
## MCMC vs. Alternatives
|
||||
|
||||
| Method | Pros | Cons |
|
||||
|--------|------|------|
|
||||
| **MCMC** | General, exact (asymptotically) | Slow convergence, diagnostics needed |
|
||||
| Variational Inference | Fast, scalable | Approximate, may be biased |
|
||||
| Importance Sampling | Simple, independent samples | Requires good proposal |
|
||||
| Grid/Quadrature | Deterministic | Exponential in dimension |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Metropolis, N. et al. (1953). "Equation of state calculations by fast computing machines." *J. Chem. Phys.*, 21(6):1087–1092.
|
||||
|
||||
2. Hastings, W.K. (1970). "Monte Carlo sampling methods using Markov chains and their applications." *Biometrika*, 57(1):97–109.
|
||||
|
||||
3. Gelfand, A.E. & Smith, A.F.M. (1990). "Sampling-based approaches to calculating marginal densities." *JASA*, 85(410):398–409.
|
||||
|
||||
4. Roberts, G.O. & Rosenthal, J.S. (2001). "Optimal scaling for various Metropolis-Hastings algorithms." *Statistical Science*, 16(4):351–367.
|
||||
|
||||
5. Brooks, S. et al. (2011). *Handbook of Markov Chain Monte Carlo*. CRC Press.
|
||||
|
||||
---
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [HMM](hmm.md) – Sequential latent variable models with EM learning
|
||||
- [Differential Evolution](differential_evolution.md) – Global optimization for finding MAP estimates
|
||||
- [Mean Field Games](mean_field_games.md) – Population dynamics with coupled PDEs
|
||||
@@ -0,0 +1,378 @@
|
||||
# Mean Field Games
|
||||
|
||||
Mean Field Games (MFG) provide a powerful framework for modeling strategic interactions
|
||||
among a large number of rational agents. Rather than tracking every individual, MFG theory
|
||||
replaces the population with a *distribution* and derives equilibrium conditions from
|
||||
coupled partial differential equations.
|
||||
|
||||
This module implements a **1D Mean Field Games solver** with a high-performance Rust
|
||||
backend exposed to Python via PyO3.
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### The State of a Representative Agent
|
||||
|
||||
Each agent's state $X_t$ evolves according to a controlled stochastic differential equation:
|
||||
|
||||
$$
|
||||
dX_t = b(X_t, \alpha_t, m_t)\,dt + \sigma\,dW_t
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $\alpha_t$ is the agent's control (decision variable)
|
||||
- $m_t$ is the population distribution at time $t$
|
||||
- $W_t$ is standard Brownian motion
|
||||
- $\sigma$ controls the diffusion intensity (related to `nu` in the solver)
|
||||
|
||||
The agent seeks to minimize expected cumulative cost:
|
||||
|
||||
$$
|
||||
J(\alpha) = \mathbb{E}\left[\int_0^T L(X_t, \alpha_t, m_t)\,dt + g(X_T)\right]
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
### The MFG System: Two Coupled PDEs
|
||||
|
||||
The MFG equilibrium is characterized by **two coupled PDEs**:
|
||||
|
||||
#### 1. Hamilton-Jacobi-Bellman (HJB) Equation — Backward in Time
|
||||
|
||||
The value function $u(x,t)$ represents the optimal cost-to-go and satisfies:
|
||||
|
||||
$$
|
||||
-\frac{\partial u}{\partial t} - \nu \frac{\partial^2 u}{\partial x^2} + H\left(x, \frac{\partial u}{\partial x}, m\right) = 0
|
||||
$$
|
||||
|
||||
**Terminal condition:** $u(x, T) = g(x)$ (terminal cost)
|
||||
|
||||
The Hamiltonian $H$ captures the running cost. For quadratic control costs:
|
||||
|
||||
$$
|
||||
H(x, p, m) = \frac{|p|^2}{2} - f(x, m)
|
||||
$$
|
||||
|
||||
where $f(x, m)$ is the congestion cost (penalizes crowded regions).
|
||||
|
||||
#### 2. Fokker-Planck (FP) Equation — Forward in Time
|
||||
|
||||
The population density $m(x,t)$ evolves according to:
|
||||
|
||||
$$
|
||||
\frac{\partial m}{\partial t} - \nu \frac{\partial^2 m}{\partial x^2} - \frac{\partial}{\partial x}\left(m \frac{\partial u}{\partial x}\right) = 0
|
||||
$$
|
||||
|
||||
**Initial condition:** $m(x, 0) = m_0(x)$ (initial population distribution)
|
||||
|
||||
This equation propagates the density forward given the optimal velocity field
|
||||
$v^*(x,t) = -\partial u / \partial x$ from the HJB solution.
|
||||
|
||||
---
|
||||
|
||||
### The Fixed-Point Loop
|
||||
|
||||
The solver uses an iterative scheme to find the coupled equilibrium:
|
||||
|
||||
```
|
||||
Algorithm: MFG Fixed-Point Iteration
|
||||
─────────────────────────────────────
|
||||
1. Initialize: m⁽⁰⁾(x,t) = m₀(x) for all t
|
||||
2. For k = 0, 1, 2, ... until convergence:
|
||||
a. Solve HJB backward: u⁽ᵏ⁺¹⁾ given m⁽ᵏ⁾
|
||||
b. Solve FP forward: m̃⁽ᵏ⁺¹⁾ given u⁽ᵏ⁺¹⁾
|
||||
c. Relax: m⁽ᵏ⁺¹⁾ = α·m̃⁽ᵏ⁺¹⁾ + (1-α)·m⁽ᵏ⁾
|
||||
d. Check: ||m⁽ᵏ⁺¹⁾ - m⁽ᵏ⁾|| < tol ?
|
||||
3. Return: (u*, m*, iterations)
|
||||
```
|
||||
|
||||
The relaxation parameter `alpha` (typically 0.3–0.7) stabilizes convergence by
|
||||
damping oscillations between iterations.
|
||||
|
||||
---
|
||||
|
||||
## Numerical Methods
|
||||
|
||||
### Discretization
|
||||
|
||||
The solver uses a finite-difference scheme on a uniform grid:
|
||||
|
||||
| Parameter | Notation | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `nx` | $N_x$ | Number of spatial grid points |
|
||||
| `nt` | $N_t$ | Number of time steps |
|
||||
| `dx` | $\Delta x = (x_{max} - x_{min}) / (N_x - 1)$ | Spatial step |
|
||||
| `dt` | $\Delta t = T / N_t$ | Time step |
|
||||
|
||||
### Stability: The CFL Condition
|
||||
|
||||
For numerical stability, the scheme requires:
|
||||
|
||||
$$
|
||||
\frac{\nu \cdot \Delta t}{(\Delta x)^2} \leq \frac{1}{2}
|
||||
$$
|
||||
|
||||
**Practical rule**: If you see oscillations or blow-up, either:
|
||||
- Increase `nt` (smaller $\Delta t$)
|
||||
- Increase `nu` (more diffusion smooths the solution)
|
||||
- Decrease `nx` (larger $\Delta x$)
|
||||
|
||||
### Transport: Upwind Differencing
|
||||
|
||||
The advection term $\partial(m \cdot v)/\partial x$ uses **upwind differencing**
|
||||
to ensure stability:
|
||||
|
||||
- If $v > 0$: use backward difference
|
||||
- If $v < 0$: use forward difference
|
||||
|
||||
This prevents numerical oscillations in steep density gradients.
|
||||
|
||||
### Diffusion: Implicit Scheme
|
||||
|
||||
The diffusion term $\nu \partial^2 m / \partial x^2$ is solved **implicitly**
|
||||
using a tridiagonal system (Thomas algorithm), making the scheme unconditionally
|
||||
stable for diffusion.
|
||||
|
||||
### Mass Conservation
|
||||
|
||||
After each Fokker-Planck step, the density is renormalized:
|
||||
|
||||
$$
|
||||
m^{(k+1)} \leftarrow \frac{m^{(k+1)}}{\int m^{(k+1)} dx}
|
||||
$$
|
||||
|
||||
This ensures $\int m(x,t)\,dx = 1$ is preserved throughout the simulation.
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
from optimizr import MFGConfig
|
||||
|
||||
config = MFGConfig(
|
||||
nx=100, # spatial grid points
|
||||
nt=100, # time steps
|
||||
x_min=0.0, # left boundary
|
||||
x_max=1.0, # right boundary
|
||||
T=1.0, # terminal time
|
||||
nu=0.01, # diffusion coefficient (viscosity)
|
||||
max_iter=50, # maximum fixed-point iterations
|
||||
tol=1e-5, # convergence tolerance
|
||||
alpha=0.5, # relaxation parameter
|
||||
)
|
||||
```
|
||||
|
||||
### Solving the MFG System
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import MFGConfig, solve_mfg_1d_rust
|
||||
|
||||
# Define spatial grid
|
||||
x = np.linspace(0, 1, 100)
|
||||
|
||||
# Initial population: Gaussian centered at x=0.3
|
||||
m0 = np.exp(-50 * (x - 0.3) ** 2)
|
||||
m0 /= np.trapz(m0, x) # normalize to unit mass
|
||||
|
||||
# Terminal cost: quadratic penalty away from x=0.7
|
||||
u_terminal = 0.5 * (x - 0.7) ** 2
|
||||
|
||||
# Create configuration
|
||||
config = MFGConfig(
|
||||
nx=100, nt=100,
|
||||
x_min=0.0, x_max=1.0, T=1.0,
|
||||
nu=0.01, max_iter=50, tol=1e-5, alpha=0.5,
|
||||
)
|
||||
|
||||
# Solve the MFG system
|
||||
u, m, iterations = solve_mfg_1d_rust(m0, u_terminal, config, lambda_congestion=0.5)
|
||||
|
||||
print(f"Converged in {iterations} iterations")
|
||||
print(f"Value function shape: {u.shape}")
|
||||
print(f"Density shape: {m.shape}")
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
Converged in 34 iterations
|
||||
Value function shape: (100, 101)
|
||||
Density shape: (100, 101)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visualization
|
||||
|
||||
### Density Evolution Heatmap
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
|
||||
|
||||
# Density heatmap
|
||||
im0 = axes[0].imshow(m.T, origin='lower', aspect='auto',
|
||||
extent=[0, 1, 0, 1], cmap='viridis')
|
||||
axes[0].set_xlabel('Position x')
|
||||
axes[0].set_ylabel('Time t')
|
||||
axes[0].set_title('Population Density m(x,t)')
|
||||
plt.colorbar(im0, ax=axes[0])
|
||||
|
||||
# Value function heatmap
|
||||
im1 = axes[1].imshow(u.T, origin='lower', aspect='auto',
|
||||
extent=[0, 1, 0, 1], cmap='plasma')
|
||||
axes[1].set_xlabel('Position x')
|
||||
axes[1].set_ylabel('Time t')
|
||||
axes[1].set_title('Value Function u(x,t)')
|
||||
plt.colorbar(im1, ax=axes[1])
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('mfg_heatmaps.png', dpi=150)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
This code generates two side-by-side heatmaps:
|
||||
- **Left plot**: Population density `m(x,t)` evolution over space and time
|
||||
- **Right plot**: Value function `u(x,t)` showing optimal value at each position and time
|
||||
|
||||
For interactive visualization with complete outputs, see the [Mean Field Games Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) on GitHub.
|
||||
|
||||
### Time Slices
|
||||
|
||||
```python
|
||||
t_indices = [0, 25, 50, 75, 100]
|
||||
colors = plt.cm.viridis(np.linspace(0, 1, len(t_indices)))
|
||||
|
||||
plt.figure(figsize=(8, 5))
|
||||
for i, t_idx in enumerate(t_indices):
|
||||
t_val = t_idx / 100.0
|
||||
plt.plot(x, m[:, t_idx], color=colors[i], label=f't={t_val:.2f}')
|
||||
|
||||
plt.xlabel('Position x')
|
||||
plt.ylabel('Density m(x,t)')
|
||||
plt.title('Population Density at Different Times')
|
||||
plt.legend()
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.savefig('mfg_time_slices.png', dpi=150)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
This produces a line plot showing population density profiles at 5 different time points (t=0.0, 0.25, 0.5, 0.75, 1.0), illustrating how the population distribution evolves from initial to terminal conditions.
|
||||
|
||||
📓 **Complete Examples**: See the [Mean Field Games Tutorial](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) for interactive visualizations with real numerical solutions.
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks on laptop-class CPU (Apple M1):
|
||||
|
||||
| Grid Size | Iterations | Time |
|
||||
|-----------|------------|------|
|
||||
| 64×40 | 28 | 0.08 s |
|
||||
| 100×100 | 34 | 0.37 s |
|
||||
| 200×200 | 41 | 2.1 s |
|
||||
| 500×500 | 52 | 18.4 s |
|
||||
|
||||
Memory usage scales as $O(N_x \times N_t)$ for storing both arrays.
|
||||
|
||||
---
|
||||
|
||||
## Convergence Diagnostics
|
||||
|
||||
### What to Monitor
|
||||
|
||||
1. **Density residual**: $\|m^{(k+1)} - m^{(k)}\|_1$ should decrease monotonically
|
||||
2. **Value residual**: $\|u^{(k+1)} - u^{(k)}\|_\infty$ should decrease
|
||||
3. **Mass conservation**: $\int m(x,t)\,dx \approx 1.0$ at all times
|
||||
4. **No oscillations**: Smooth density profiles without wiggles
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Slow convergence | `alpha` too small | Increase to 0.6–0.7 |
|
||||
| Oscillating residuals | `alpha` too large | Decrease to 0.3–0.4 |
|
||||
| Numerical blow-up | CFL violation | Increase `nt` or `nu` |
|
||||
| Density spikes | Weak diffusion | Increase `nu` or `lambda_congestion` |
|
||||
| Negative densities | Upwind instability | Increase `nu` |
|
||||
|
||||
---
|
||||
|
||||
## The Congestion Term
|
||||
|
||||
The parameter `lambda_congestion` controls crowd aversion:
|
||||
|
||||
$$
|
||||
f(x, m) = \lambda \cdot m(x)^{\gamma}
|
||||
$$
|
||||
|
||||
| `lambda_congestion` | Effect |
|
||||
|---------------------|--------|
|
||||
| 0.0 | No interaction; agents ignore each other |
|
||||
| 0.1–0.5 | Mild spreading; prefer less crowded regions |
|
||||
| 1.0+ | Strong dispersion; density stays nearly uniform |
|
||||
|
||||
Higher values prevent density spikes but may slow convergence.
|
||||
|
||||
---
|
||||
|
||||
## Practical Tips
|
||||
|
||||
### Grid Resolution
|
||||
|
||||
- **Prototyping**: `nx=64, nt=40` — fast iteration, rough results
|
||||
- **Publication**: `nx=100, nt=100` — good balance of speed and quality
|
||||
- **High-fidelity**: `nx=200, nt=200` — smooth gradients, longer runtime
|
||||
|
||||
### Parameter Tuning
|
||||
|
||||
1. Start with `nu=0.01, alpha=0.5, lambda_congestion=0.5`
|
||||
2. If convergence is slow, try `alpha=0.7`
|
||||
3. If density has spikes, increase `lambda_congestion` to 1.0
|
||||
4. If numerical issues appear, increase `nu` to 0.02–0.05
|
||||
|
||||
### Initial Conditions
|
||||
|
||||
Good choices for `m0`:
|
||||
- **Gaussian**: `np.exp(-50 * (x - x0)**2)` — localized starting distribution
|
||||
- **Uniform**: `np.ones(nx) / nx` — spread-out initial population
|
||||
- **Bimodal**: Sum of two Gaussians — models two subpopulations
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Lasry, J.-M. and Lions, P.-L. (2007). "Mean field games." *Japanese Journal of Mathematics*, 2(1):229–260.
|
||||
|
||||
2. Cardaliaguet, P. (2013). "Notes on Mean Field Games." Lecture notes, Collège de France.
|
||||
|
||||
3. Achdou, Y. and Capuzzo-Dolcetta, I. (2010). "Mean field games: numerical methods." *SIAM Journal on Numerical Analysis*, 48(3):1136–1162.
|
||||
|
||||
4. Huang, M., Malhamé, R., and Caines, P. (2006). "Large population stochastic dynamic games: closed-loop McKean-Vlasov systems and the Nash certainty equivalence principle." *Communications in Information and Systems*, 6(3):221–252.
|
||||
|
||||
---
|
||||
|
||||
## Notebook Tutorial
|
||||
|
||||
For a complete walkthrough with validated outputs and visualizations, see the
|
||||
[Mean Field Games Tutorial notebook](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/mean_field_games_tutorial.ipynb) on GitHub.
|
||||
|
||||
The notebook demonstrates:
|
||||
|
||||
- Setting up initial distributions
|
||||
- Running the solver with different parameters
|
||||
- Visualizing density evolution as 3D surfaces and heatmaps
|
||||
- Interpreting convergence diagnostics
|
||||
- Comparing congestion levels
|
||||
|
||||
Audit documentation is available at [`docs/MFG_TUTORIAL_COMPLETE.md`](https://github.com/ThotDjehuty/optimiz-r/blob/main/docs/MFG_TUTORIAL_COMPLETE.md).
|
||||
@@ -0,0 +1,48 @@
|
||||
Asynchronous Covariance (Hayashi--Yoshida)
|
||||
==========================================
|
||||
|
||||
The module :code:`timeseries_utils::nonsync_covariance` implements the
|
||||
Hayashi--Yoshida estimator of the integrated covariance between two
|
||||
asynchronously sampled processes :math:`X` and :math:`Y` observed at
|
||||
distinct, non-overlapping observation grids
|
||||
:math:`\{t^X_i\}` and :math:`\{t^Y_j\}`.
|
||||
|
||||
Estimator
|
||||
---------
|
||||
|
||||
Let :math:`I_i = (t^X_{i-1}, t^X_i]` and :math:`J_j = (t^Y_{j-1}, t^Y_j]`.
|
||||
The Hayashi--Yoshida estimator is
|
||||
|
||||
.. math::
|
||||
|
||||
\widehat{\langle X, Y\rangle}_{[0,T]}
|
||||
\;=\;
|
||||
\sum_{i, j}\,
|
||||
\big(X_{t^X_i} - X_{t^X_{i-1}}\big)\,
|
||||
\big(Y_{t^Y_j} - Y_{t^Y_{j-1}}\big)\,
|
||||
\mathbf{1}\!\big[I_i \cap J_j \neq \emptyset\big].
|
||||
|
||||
It is consistent under non-synchronicity and avoids the *Epps effect*
|
||||
that plagues naive grid interpolation.
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
* A two-pointer scan in :math:`O(n_X + n_Y)` collects all overlapping
|
||||
pairs.
|
||||
* For matrices of size :math:`d \times d` with large per-asset sample
|
||||
counts, off-diagonal entries are computed in parallel with Rayon.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub fn hayashi_yoshida_covariance(
|
||||
t1: &[f64], v1: &[f64],
|
||||
t2: &[f64], v2: &[f64],
|
||||
) -> Result<f64>;
|
||||
|
||||
pub fn hayashi_yoshida_matrix(
|
||||
series: &[(Vec<f64>, Vec<f64>)],
|
||||
) -> Result<Vec<Vec<f64>>>;
|
||||
@@ -0,0 +1,610 @@
|
||||
# Optimal Control
|
||||
|
||||
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.
|
||||
|
||||
## 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, # 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"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)")
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
# 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**: Maximum likelihood estimation (MLE) using analytical formulas for discrete-time OU process.
|
||||
|
||||
**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
|
||||
from optimizr import LinearKalmanFilter
|
||||
import numpy as np
|
||||
|
||||
# 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,
|
||||
q_matrix=Q,
|
||||
r_matrix=R,
|
||||
initial_state=[0.0, 0.0],
|
||||
initial_covariance=[[1.0, 0.0], [0.0, 1.0]],
|
||||
)
|
||||
|
||||
# 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]}")
|
||||
```
|
||||
|
||||
**Use cases**:
|
||||
- Tracking latent spread dynamics with noise
|
||||
- State estimation for control (e.g., estimate velocity from noisy position)
|
||||
- Online parameter adaptation
|
||||
|
||||
#### 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
|
||||
@@ -0,0 +1,94 @@
|
||||
# Optimal Control
|
||||
|
||||
Hamilton–Jacobi–Bellman (HJB) solvers, regime-switching thresholds, OU parameter estimation, and Kalman filtering utilities backed by Rust.
|
||||
|
||||
## HJB switching boundaries (OU process)
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```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
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
```python
|
||||
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.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Point Processes & Fractional Brownian Motion
|
||||
|
||||
This module implements the mathematical framework from **Muhle-Karbe, Jusselin & Rosenbaum** (2022) for modeling order flow microstructure through self-exciting point processes and fractional dynamics.
|
||||
|
||||
It provides high-performance Rust implementations of:
|
||||
- **Hawkes Processes** with flexible excitation kernels
|
||||
- **Fractional Brownian Motion (fBM)** with exact simulation
|
||||
- **Mixed Fractional Brownian Motion (mfBM)** for aggregate flow
|
||||
- **Mittag-Leffler Functions** for scaling limit analysis
|
||||
|
||||
---
|
||||
|
||||
## Mathematical Foundations
|
||||
|
||||
### The Unified Theory of Order Flow
|
||||
|
||||
The key insight from the unified theory is that a **single parameter** — the Hurst exponent $H_0 \approx 3/4$ — governs all market microstructure quantities:
|
||||
|
||||
$$
|
||||
\boxed{H_0 \approx \frac{3}{4}}
|
||||
$$
|
||||
|
||||
This parameter determines:
|
||||
|
||||
| Quantity | Formula | Value at $H_0 = 3/4$ |
|
||||
|----------|---------|----------------------|
|
||||
| Price roughness | $H_{\text{price}} = H_0 - \tfrac{1}{2}$ | $1/4$ |
|
||||
| Volatility roughness | $H_{\text{vol}} \approx H_0 - \tfrac{1}{2}$ | $\approx 0.1$ |
|
||||
| Market impact exponent | $\delta = 1 - \tfrac{1}{2H_0}$ | $1/3$ |
|
||||
| Kyle's lambda | $\Lambda \sim n^{-\delta}$ | $\sim n^{-1/3}$ |
|
||||
| Kernel tail exponent | $\alpha_0 = H_0/2$ | $3/8$ |
|
||||
|
||||
The model structure is:
|
||||
|
||||
$$
|
||||
N = F + R
|
||||
$$
|
||||
|
||||
where:
|
||||
- $N$ = total order flow (observable)
|
||||
- $F$ = core (fundamental) order flow
|
||||
- $R$ = reaction (self-exciting) order flow modeled by Hawkes processes
|
||||
|
||||
---
|
||||
|
||||
## Hawkes Processes
|
||||
|
||||
### Definition
|
||||
|
||||
A (univariate) Hawkes process $N(t)$ has conditional intensity:
|
||||
|
||||
$$
|
||||
\lambda(t) = \nu + \int_0^{t^-} \phi(t - s) \, dN(s) = \nu + \sum_{t_i < t} \phi(t - t_i)
|
||||
$$
|
||||
|
||||
where:
|
||||
- $\nu > 0$ is the **baseline intensity** (exogenous arrival rate)
|
||||
- $\phi: \mathbb{R}_+ \to \mathbb{R}_+$ is the **excitation kernel** (self-exciting memory)
|
||||
- $t_i$ are past event times
|
||||
|
||||
The process is **stable** (stationary) when the **branching ratio** satisfies:
|
||||
|
||||
$$
|
||||
\|\phi\|_{L^1} = \int_0^\infty \phi(t) \, dt < 1
|
||||
$$
|
||||
|
||||
The expected number of events per unit time in stationarity is:
|
||||
|
||||
$$
|
||||
\mathbb{E}[\lambda] = \frac{\nu}{1 - \|\phi\|_{L^1}}
|
||||
$$
|
||||
|
||||
### Excitation Kernels
|
||||
|
||||
#### Exponential Kernel (Short Memory)
|
||||
|
||||
$$
|
||||
\phi(t) = \alpha \, e^{-\beta t}, \quad \alpha, \beta > 0
|
||||
$$
|
||||
|
||||
Properties:
|
||||
- **L¹ norm**: $\|\phi\|_{L^1} = \alpha / \beta$
|
||||
- **Stability**: $\alpha < \beta$
|
||||
- **Half-life**: $t_{1/2} = \ln 2 / \beta$
|
||||
- **Tail**: exponential decay (no long memory)
|
||||
- **Characteristic timescale**: $\tau = 1/\beta$
|
||||
|
||||
The exponential kernel leads to an intensity process that is Markovian — the full history can be summarized by the current intensity level. The integrated kernel is:
|
||||
|
||||
$$
|
||||
\int_0^t \phi(s) \, ds = \frac{\alpha}{\beta} \left(1 - e^{-\beta t}\right)
|
||||
$$
|
||||
|
||||
#### Power-Law Kernel (Long Memory)
|
||||
|
||||
$$
|
||||
\phi(t) = K_0 \, (1 + t)^{-(1 + \alpha_0)}, \quad K_0 > 0, \; \alpha_0 \in (0, 1)
|
||||
$$
|
||||
|
||||
Properties:
|
||||
- **L¹ norm**: $\|\phi\|_{L^1} = K_0 / \alpha_0$
|
||||
- **Stability**: $K_0 < \alpha_0$
|
||||
- **Tail exponent**: $\alpha_0$ controls memory persistence
|
||||
- **Hurst connection**: $H_0 = 2\alpha_0$ (from the unified theory)
|
||||
- **Long memory**: polynomial decay produces clustering at all timescales
|
||||
|
||||
The integrated kernel is:
|
||||
|
||||
$$
|
||||
\int_0^t \phi(s) \, ds = \frac{K_0}{\alpha_0} \left[1 - (1 + t)^{-\alpha_0}\right]
|
||||
$$
|
||||
|
||||
The **critical** regime ($\|\phi\|_{L^1} = 1$) corresponds to $K_0 = \alpha_0$, and the **nearly-critical** regime ($\|\phi\|_{L^1} = 1 - \varepsilon$) is relevant for real market data where the branching ratio is very close to 1.
|
||||
|
||||
#### Completely Monotone Kernel (Assumption A)
|
||||
|
||||
From the unified theory paper's **Assumption A**, the most general kernel satisfying the scaling limit theorems:
|
||||
|
||||
$$
|
||||
\phi(t) = K_0 \, t^{-\alpha_0} \, E_{1-\alpha_0}\!\left(-\lambda \, t^{1-\alpha_0}\right)
|
||||
$$
|
||||
|
||||
where $E_\alpha$ is the Mittag-Leffler function. This kernel:
|
||||
- Is **completely monotone** on $(0, \infty)$
|
||||
- Interpolates between power-law and exponential behavior
|
||||
- Satisfies all conditions for the scaling limit theorems
|
||||
|
||||
### Simulation: Ogata's Thinning Algorithm
|
||||
|
||||
The Hawkes process is simulated using **Ogata's thinning algorithm**:
|
||||
|
||||
1. Compute upper bound $\lambda_{\max} \geq \lambda(t)$ for the current intensity
|
||||
2. Generate candidate inter-arrival time $\tau \sim \text{Exp}(\lambda_{\max})$
|
||||
3. Accept with probability $\lambda(t + \tau) / \lambda_{\max}$
|
||||
4. If rejected, advance time to $t + \tau$ and repeat
|
||||
|
||||
The algorithm has expected time complexity $O(n \log n)$ where $n$ is the number of events.
|
||||
|
||||
### Maximum Likelihood Estimation
|
||||
|
||||
The log-likelihood of a Hawkes process on $[0, T]$ with event times $\{t_1, \ldots, t_n\}$:
|
||||
|
||||
$$
|
||||
\ell(\boldsymbol{\theta}) = \sum_{i=1}^n \log \lambda(t_i) - \int_0^T \lambda(t) \, dt
|
||||
$$
|
||||
|
||||
The compensator (integrated intensity) decomposes as:
|
||||
|
||||
$$
|
||||
\int_0^T \lambda(t) \, dt = \nu T + \sum_{i=1}^n \int_0^{T - t_i} \phi(s) \, ds
|
||||
$$
|
||||
|
||||
### Bivariate Hawkes Process
|
||||
|
||||
For order flow modeling, buy and sell reaction orders follow a **bivariate Hawkes process** $\mathbf{N} = (N^+, N^-)$ with intensity:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\lambda^+(t) &= \mu^+(t) + \int \left[\phi_1(t-s) \, dN^+(s) + \phi_2(t-s) \, dN^-(s)\right] \\
|
||||
\lambda^-(t) &= \mu^-(t) + \int \left[\phi_2(t-s) \, dN^+(s) + \phi_1(t-s) \, dN^-(s)\right]
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
where:
|
||||
- $\phi_1$: **self-excitation** kernel (buy $\to$ buy, sell $\to$ sell)
|
||||
- $\phi_2$: **cross-excitation** kernel (buy $\to$ sell, sell $\to$ buy)
|
||||
- $\mu^\pm(t)$: baselines driven by core order flow
|
||||
|
||||
The **stability condition** requires the spectral radius of the kernel matrix:
|
||||
|
||||
$$
|
||||
\rho\!\left(\begin{pmatrix} \|\phi_1\|_1 & \|\phi_2\|_1 \\ \|\phi_2\|_1 & \|\phi_1\|_1 \end{pmatrix}\right) = \|\phi_1\|_1 + \|\phi_2\|_1 < 1
|
||||
$$
|
||||
|
||||
The **signed flow** $N^+(t) - N^-(t)$ captures the net order imbalance driving price changes, while the **unsigned volume** $N^+(t) + N^-(t)$ measures total reaction activity.
|
||||
|
||||
---
|
||||
|
||||
## Fractional Brownian Motion
|
||||
|
||||
### Definition
|
||||
|
||||
Fractional Brownian motion (fBM) $B^H_t$ with **Hurst parameter** $H \in (0, 1)$ is the unique centered Gaussian process with:
|
||||
|
||||
$$
|
||||
\text{Cov}(B^H_s, B^H_t) = \frac{1}{2}\left(|t|^{2H} + |s|^{2H} - |t-s|^{2H}\right)
|
||||
$$
|
||||
|
||||
Key properties:
|
||||
- **Self-similarity**: $B^H_{ct} \overset{d}{=} c^H B^H_t$ for all $c > 0$
|
||||
- **Stationary increments**: $B^H_t - B^H_s \overset{d}{=} B^H_{t-s}$
|
||||
- **Variance**: $\text{Var}(B^H_t) = t^{2H}$
|
||||
|
||||
The three regimes are:
|
||||
|
||||
| Range | Behavior | Autocorrelation | Financial Interpretation |
|
||||
|-------|----------|----------------|------------------------|
|
||||
| $H < 1/2$ | **Anti-persistent** (mean-reverting) | Negative | Price reversals dominate |
|
||||
| $H = 1/2$ | **Standard BM** (no memory) | Zero | Random walk |
|
||||
| $H > 1/2$ | **Persistent** (trending) | Positive | Trends persist |
|
||||
|
||||
### Fractional Gaussian Noise (fGn)
|
||||
|
||||
The increments of fBM form **fractional Gaussian noise** with autocovariance:
|
||||
|
||||
$$
|
||||
\gamma(k) = \frac{1}{2}\left(|k-1|^{2H} - 2|k|^{2H} + |k+1|^{2H}\right)
|
||||
$$
|
||||
|
||||
For $H > 1/2$, $\gamma(k) > 0$ for all $k$, indicating **long-range dependence**:
|
||||
|
||||
$$
|
||||
\sum_{k=0}^\infty \gamma(k) = \infty
|
||||
$$
|
||||
|
||||
### Simulation Methods
|
||||
|
||||
#### Cholesky Method
|
||||
|
||||
Exact simulation by forming the covariance matrix $\Sigma$ and computing its Cholesky decomposition:
|
||||
|
||||
$$
|
||||
\Sigma = L L^\top, \quad \mathbf{B}^H = L \mathbf{Z}, \quad \mathbf{Z} \sim \mathcal{N}(\mathbf{0}, I_n)
|
||||
$$
|
||||
|
||||
Complexity: $O(n^3)$ for decomposition, $O(n^2)$ for simulation.
|
||||
|
||||
#### Hosking's Method (Durbin-Levinson)
|
||||
|
||||
For regular time grids, uses the **Durbin-Levinson algorithm** to compute prediction coefficients for the fGn, then reconstructs fBM by cumulative summation:
|
||||
|
||||
1. Compute autocovariance sequence $\gamma(0), \gamma(1), \ldots, \gamma(n-1)$
|
||||
2. Recursively compute Levinson coefficients $\phi_{i,j}$ and prediction variances $v_i$
|
||||
3. Generate fGn: $X_i = \sum_{j=0}^{i-1} \phi_{i,j} X_{i-1-j} + \sqrt{v_i} Z_i$
|
||||
4. Cumulate: $B^H_k = \sum_{i=0}^{k-1} X_i \cdot (\Delta t)^H$
|
||||
|
||||
Complexity: $O(n^2)$ — more efficient than Cholesky for large $n$.
|
||||
|
||||
### Hurst Exponent Estimation
|
||||
|
||||
#### Rescaled Range (R/S) Analysis
|
||||
|
||||
The R/S statistic for a subseries of length $n$:
|
||||
|
||||
$$
|
||||
(R/S)_n = \frac{\max_{1 \leq k \leq n} W_k - \min_{1 \leq k \leq n} W_k}{S_n}
|
||||
$$
|
||||
|
||||
where $W_k = \sum_{i=1}^k (X_i - \bar{X})$ is the cumulative deviation and $S_n$ is the standard deviation.
|
||||
|
||||
For fBM/fGn:
|
||||
|
||||
$$
|
||||
\mathbb{E}[(R/S)_n] \sim c \cdot n^H \quad \text{as } n \to \infty
|
||||
$$
|
||||
|
||||
The Hurst exponent is estimated by linear regression of $\log(R/S)$ against $\log n$:
|
||||
|
||||
$$
|
||||
\hat{H} = \frac{\sum_i (\log n_i - \overline{\log n})(\log(R/S)_i - \overline{\log(R/S)})}{\sum_i (\log n_i - \overline{\log n})^2}
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
## Mixed Fractional Brownian Motion
|
||||
|
||||
### Definition
|
||||
|
||||
The mixed fBM (mfBM) combines a standard BM with an independent fBM:
|
||||
|
||||
$$
|
||||
M^H(t) = a \cdot B(t) + b \cdot B^H(t)
|
||||
$$
|
||||
|
||||
where:
|
||||
- $B(t)$: standard Brownian motion (diffusive component)
|
||||
- $B^H(t)$: fractional BM with Hurst index $H$
|
||||
- $a, b$: mixing coefficients
|
||||
|
||||
### Covariance Structure
|
||||
|
||||
$$
|
||||
\text{Cov}(M^H_s, M^H_t) = a^2 \min(s,t) + \frac{b^2}{2}\left(|t|^{2H} + |s|^{2H} - |t-s|^{2H}\right)
|
||||
$$
|
||||
|
||||
### Role in the Unified Theory
|
||||
|
||||
In the scaling limit of the Hawkes-based order flow model, the aggregate order flow converges to:
|
||||
|
||||
$$
|
||||
\frac{1}{\sqrt{n}} \sum_{i=1}^{\lfloor nt \rfloor} (N^+_i - N^-_i) \xrightarrow{d} \sigma_F \cdot M^{H_0}(t)
|
||||
$$
|
||||
|
||||
where the Hurst exponent $H_0 = 2\alpha_0$ is determined by the kernel tail.
|
||||
|
||||
### Semimartingale Property
|
||||
|
||||
The mfBM is a **semimartingale** if and only if $H > 3/4$. This has pricing implications:
|
||||
- For $H > 3/4$: classical stochastic calculus applies, no arbitrage
|
||||
- For $H \leq 3/4$: not a semimartingale, requires fractional calculus
|
||||
|
||||
### Scale-Dependent Hurst Analysis
|
||||
|
||||
To identify a mfBM (vs pure fBM or BM), examine the **scale-dependent Hurst exponent**:
|
||||
|
||||
$$
|
||||
H(\Delta) = \frac{1}{2} \cdot \frac{\log \text{Var}[X(t+2\Delta) - X(t)]}{\log \text{Var}[X(t+\Delta) - X(t)]} \cdot \frac{1}{\log 2}
|
||||
$$
|
||||
|
||||
For pure fBM, $H(\Delta) \approx H$ at all scales. For mfBM:
|
||||
- **Short timescales**: $H(\Delta) \to 1/2$ (BM dominates)
|
||||
- **Long timescales**: $H(\Delta) \to H$ (fBM dominates)
|
||||
|
||||
This crossover behavior is a hallmark of the mixed process and matches empirical observations in order flow data.
|
||||
|
||||
---
|
||||
|
||||
## Mittag-Leffler Functions
|
||||
|
||||
### Definition
|
||||
|
||||
The generalized Mittag-Leffler function:
|
||||
|
||||
$$
|
||||
E_{\alpha,\beta}(z) = \sum_{k=0}^\infty \frac{z^k}{\Gamma(\alpha k + \beta)}, \quad \alpha > 0, \; \beta > 0
|
||||
$$
|
||||
|
||||
Special cases:
|
||||
- $E_{1,1}(z) = e^z$ (exponential function)
|
||||
- $E_{2,1}(z^2) = \cosh(z)$ (hyperbolic cosine)
|
||||
- $E_{1,2}(z) = (e^z - 1)/z$ (exponential integral)
|
||||
|
||||
### Asymptotic Behavior
|
||||
|
||||
For $0 < \alpha < 1$ and large $|z|$:
|
||||
|
||||
$$
|
||||
E_{\alpha,\beta}(z) \sim \begin{cases}
|
||||
\frac{1}{\alpha} z^{(1-\beta)/\alpha} \exp\!\left(z^{1/\alpha}\right) & z \to +\infty \\[6pt]
|
||||
-\sum_{k=1}^{p} \frac{z^{-k}}{\Gamma(\beta - \alpha k)} + O(|z|^{-p-1}) & z \to -\infty
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
### The $f_{\alpha_0, \lambda_0}$ Function
|
||||
|
||||
From **Theorem 3.1** of Muhle-Karbe et al., the key scaling function:
|
||||
|
||||
$$
|
||||
f_{\alpha_0, \lambda_0}(x) = \lambda_0 \, x^{\alpha_0 - 1} \, E_{\alpha_0, \alpha_0}\!\left(-\lambda_0 \, x^{\alpha_0}\right)
|
||||
$$
|
||||
|
||||
This function controls how the Hawkes process's self-excitation structure manifests in the scaling limit. Its integral satisfies:
|
||||
|
||||
$$
|
||||
\int_0^t f_{\alpha_0, \lambda_0}(s) \, ds = t^{\alpha_0} \, E_{\alpha_0, \alpha_0 + 1}\!\left(-\lambda_0 \, t^{\alpha_0}\right)
|
||||
$$
|
||||
|
||||
The function $f_{\alpha_0, \lambda_0}$ interpolates between:
|
||||
- **Short times**: $f(x) \sim \lambda_0 x^{\alpha_0 - 1}$ (power-law singularity)
|
||||
- **Long times**: $f(x) \sim x^{-1-\alpha_0}$ (power-law decay like the kernel)
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Simulating a Hawkes Process
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Simulate with exponential kernel
|
||||
events_exp = optimizr.simulate_hawkes(
|
||||
baseline=1.0, # ν = 1.0
|
||||
alpha=0.5, # α = 0.5
|
||||
beta=1.0, # β = 1.0
|
||||
t_max=100.0,
|
||||
kernel_type="exponential",
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Simulate with power-law kernel (H₀ ≈ 0.75)
|
||||
events_pl = optimizr.simulate_hawkes(
|
||||
baseline=0.1,
|
||||
alpha=0.35, # K₀ = 0.35
|
||||
beta=0.375, # α₀ = 0.375 → H₀ = 2 × 0.375 = 0.75
|
||||
t_max=100.0,
|
||||
kernel_type="power_law",
|
||||
seed=42
|
||||
)
|
||||
|
||||
print(f"Exponential kernel: {len(events_exp)} events")
|
||||
print(f"Power-law kernel: {len(events_pl)} events")
|
||||
```
|
||||
|
||||
### Bivariate Buy/Sell Reaction Flow
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Generate core order flow (Poisson driver)
|
||||
rng = np.random.default_rng(42)
|
||||
core_buys = np.sort(rng.uniform(0, 100, 200))
|
||||
core_sells = np.sort(rng.uniform(0, 100, 180))
|
||||
|
||||
# Simulate bivariate Hawkes reaction flow
|
||||
buy_times, sell_times = optimizr.simulate_bivariate_hawkes(
|
||||
core_buy_times=core_buys,
|
||||
core_sell_times=core_sells,
|
||||
phi1_alpha=0.3, # Self-excitation (buy→buy, sell→sell)
|
||||
phi1_beta=1.0,
|
||||
phi2_alpha=0.2, # Cross-excitation (buy→sell, sell→buy)
|
||||
phi2_beta=1.0,
|
||||
t_max=100.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
print(f"Reaction buys: {len(buy_times)}, Reaction sells: {len(sell_times)}")
|
||||
print(f"Net order imbalance: {len(buy_times) - len(sell_times)}")
|
||||
|
||||
# Check stability
|
||||
l1_phi1 = 0.3 / 1.0 # L¹ norm of self-excitation
|
||||
l1_phi2 = 0.2 / 1.0 # L¹ norm of cross-excitation
|
||||
spectral_radius = l1_phi1 + l1_phi2
|
||||
print(f"Spectral radius: {spectral_radius:.2f} ({'stable' if spectral_radius < 1 else 'UNSTABLE'})")
|
||||
```
|
||||
|
||||
### Simulating Fractional Brownian Motion
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Simulate fBM paths with different Hurst exponents
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
||||
|
||||
for i, h in enumerate([0.3, 0.5, 0.8]):
|
||||
path = optimizr.simulate_fbm(hurst=h, n=1000, dt=0.01, seed=42)
|
||||
|
||||
# Estimate Hurst exponent from the path
|
||||
h_est = optimizr.estimate_hurst(path)
|
||||
|
||||
axes[i].plot(path, linewidth=0.5)
|
||||
axes[i].set_title(f"H = {h:.1f} (estimated: {h_est:.3f})")
|
||||
axes[i].set_xlabel("Time step")
|
||||
|
||||
plt.suptitle("Fractional Brownian Motion Paths")
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||
### Mixed fBM for Aggregate Order Flow
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Simulate mixed fBM (BM + fBM with H₀ = 0.75)
|
||||
path = optimizr.simulate_mixed_fbm(
|
||||
a=1.0, # BM coefficient
|
||||
b=1.0, # fBM coefficient
|
||||
hurst=0.75, # H₀ from unified theory
|
||||
n=5000,
|
||||
dt=0.01,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Scale-dependent Hurst analysis (identifies mfBM vs pure fBM)
|
||||
scales = [10, 50, 100, 500, 1000, 2000]
|
||||
hurst_by_scale = optimizr.scale_dependent_hurst(
|
||||
data=path,
|
||||
scales=scales
|
||||
)
|
||||
|
||||
print("Scale-Dependent Hurst Exponents:")
|
||||
print("-" * 35)
|
||||
for scale, h in sorted(hurst_by_scale.items()):
|
||||
print(f" Scale {scale:>5d}: H = {h:.4f}")
|
||||
```
|
||||
|
||||
### Mittag-Leffler and Scaling Functions
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Verify E_{1,1}(z) = exp(z)
|
||||
z = 2.0
|
||||
ml_value = optimizr.mittag_leffler_py(
|
||||
alpha=1.0, beta=1.0, z=z
|
||||
)
|
||||
print(f"E_{{1,1}}({z}) = {ml_value:.6f}")
|
||||
print(f"exp({z}) = {np.exp(z):.6f}")
|
||||
|
||||
# Plot the scaling function f_{α₀,λ₀}(x)
|
||||
x = np.linspace(0.01, 10, 500)
|
||||
alpha_0 = 0.375 # From H₀ = 0.75
|
||||
lambda_0 = 1.0
|
||||
|
||||
f_values = [optimizr.f_alpha_lambda_py(alpha_0, lambda_0, xi) for xi in x]
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.subplot(1, 2, 1)
|
||||
plt.plot(x, f_values)
|
||||
plt.xlabel('x')
|
||||
plt.ylabel(r'$f_{\alpha_0, \lambda_0}(x)$')
|
||||
plt.title(f'Scaling Function (α₀={alpha_0}, λ₀={lambda_0})')
|
||||
|
||||
plt.subplot(1, 2, 2)
|
||||
plt.loglog(x, np.abs(f_values))
|
||||
plt.xlabel('x (log)')
|
||||
plt.ylabel(r'$|f_{\alpha_0, \lambda_0}(x)|$ (log)')
|
||||
plt.title('Power-law decay in scaling limit')
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theoretical References
|
||||
|
||||
1. **Muhle-Karbe, Jusselin & Rosenbaum** (2022). *A unified approach to the analysis of high-frequency financial markets and limit order books.* Annals of Applied Probability.
|
||||
|
||||
2. **Jaisson & Rosenbaum** (2015). *Limit theorems for nearly unstable Hawkes processes.* Annals of Applied Probability, 25(2), 600-631.
|
||||
|
||||
3. **Bacry, Mastromatteo & Muzy** (2015). *Hawkes processes in finance.* Market Microstructure and Liquidity, 1(01), 1550005.
|
||||
|
||||
4. **Mandelbrot & Van Ness** (1968). *Fractional Brownian motions, fractional noises and applications.* SIAM Review, 10(4), 422-437.
|
||||
|
||||
5. **Gatheral, Jaisson & Rosenbaum** (2018). *Volatility is rough.* Quantitative Finance, 18(6), 933-949.
|
||||
|
||||
6. **Ogata** (1981). *On Lewis' simulation method for point processes.* IEEE Transactions on Information Theory, 27(1), 23-31.
|
||||
|
||||
7. **Hosking** (1984). *Modeling persistence in hydrological time series using fractional differencing.* Water Resources Research, 20(12), 1898-1908.
|
||||
@@ -0,0 +1,73 @@
|
||||
Risk Measures: VaR and CVaR
|
||||
============================
|
||||
|
||||
The module :code:`risk_measures` provides Value-at-Risk and Conditional
|
||||
Value-at-Risk estimators together with a convex CVaR minimisation
|
||||
solver over the unit simplex.
|
||||
|
||||
Definitions
|
||||
-----------
|
||||
|
||||
For a real random variable :math:`L` (a *loss*), the Value-at-Risk at
|
||||
confidence level :math:`\alpha \in (0, 1)` is the lower :math:`\alpha`-
|
||||
quantile
|
||||
|
||||
.. math::
|
||||
|
||||
\mathrm{VaR}_\alpha(L)
|
||||
\;=\;
|
||||
\inf\!\big\{ \ell \in \mathbb{R} : \mathbb{P}(L \le \ell) \ge \alpha \big\}.
|
||||
|
||||
The Conditional Value-at-Risk (also called Average Value-at-Risk) is
|
||||
|
||||
.. math::
|
||||
|
||||
\mathrm{CVaR}_\alpha(L)
|
||||
\;=\;
|
||||
\frac{1}{1-\alpha}\,
|
||||
\int_\alpha^1 \mathrm{VaR}_u(L)\,du.
|
||||
|
||||
For a sample :math:`L_1, \dots, L_n` of i.i.d. losses sorted in increasing
|
||||
order, the empirical CVaR at level :math:`\alpha` is
|
||||
|
||||
.. math::
|
||||
|
||||
\widehat{\mathrm{CVaR}}_\alpha
|
||||
\;=\;
|
||||
\frac{1}{n - k}\, \sum_{i = k+1}^{n} L_{(i)},
|
||||
\qquad k = \lfloor \alpha\, n \rfloor.
|
||||
|
||||
Convex minimisation
|
||||
-------------------
|
||||
|
||||
Rockafellar--Uryasev (2000) showed that
|
||||
|
||||
.. math::
|
||||
|
||||
\mathrm{CVaR}_\alpha(L)
|
||||
\;=\;
|
||||
\min_{\zeta \in \mathbb{R}}\;
|
||||
\zeta + \frac{1}{1 - \alpha}\,\mathbb{E}\!\big[(L - \zeta)_+\big].
|
||||
|
||||
Given samples of a vector :math:`r^{(s)} \in \mathbb{R}^d`,
|
||||
:code:`minimize_cvar` solves
|
||||
|
||||
.. math::
|
||||
|
||||
\min_{w \in \Delta_d,\;\zeta \in \mathbb{R}}\;
|
||||
\zeta + \frac{1}{(1 - \alpha)\, S}\, \sum_{s=1}^S
|
||||
\big(\zeta - \langle r^{(s)}, w\rangle\big)_+,
|
||||
|
||||
over the unit simplex :math:`\Delta_d`, by a projected sub-gradient
|
||||
method using the Held--Wolfe--Crowder simplex projection.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub fn historical_var(losses: &[f64], alpha: f64) -> Result<f64>;
|
||||
pub fn parametric_var(mu: f64, sigma: f64, alpha: f64) -> Result<f64>;
|
||||
pub fn cvar_value(losses: &[f64], alpha: f64) -> Result<f64>;
|
||||
pub fn minimize_cvar(returns: ArrayView2<f64>, cfg: &CVaRConfig)
|
||||
-> Result<CVaRResult>;
|
||||
@@ -0,0 +1,36 @@
|
||||
# Risk Metrics
|
||||
|
||||
Time-series utilities for risk analysis, mean-reversion detection, and bootstrapped P&L distributions.
|
||||
|
||||
## Quick start
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import (
|
||||
hurst_exponent_py,
|
||||
estimate_half_life_py,
|
||||
bootstrap_returns_py,
|
||||
compute_risk_metrics_py,
|
||||
)
|
||||
|
||||
returns = np.random.randn(2000) * 0.01
|
||||
print("Hurst:", hurst_exponent_py(returns))
|
||||
print("Half-life:", estimate_half_life_py(returns))
|
||||
|
||||
metrics = compute_risk_metrics_py(returns)
|
||||
print(metrics) # mean, std, skew, kurtosis, sharpe
|
||||
|
||||
bootstrapped = bootstrap_returns_py(returns, n_samples=1000)
|
||||
print("Bootstrap samples:", len(bootstrapped))
|
||||
```
|
||||
|
||||
## Rolling and integration helpers
|
||||
|
||||
- Use `rolling_hurst_exponent_py` and `rolling_half_life_py` (from `timeseries_utils`) for sliding-window diagnostics on trading pairs.
|
||||
- Combine with HMM: feed rolling statistics as features for regime detection.
|
||||
- Pair with DE/Grid search: optimize strategy thresholds while computing half-life inside the objective.
|
||||
|
||||
## Practical guidance
|
||||
- Input should be 1D NumPy arrays of returns; winsorize extreme tails before estimating Hurst/half-life for stability.
|
||||
- Half-life helps size holding periods for mean-reversion trades; revisit whenever volatility regime changes.
|
||||
- Bootstrap outputs can feed VaR/ES estimates; increase `n_samples` for tighter confidence bands.
|
||||
@@ -0,0 +1,119 @@
|
||||
Path Signatures
|
||||
===============
|
||||
|
||||
The module :code:`signatures` provides truncated tensor signatures
|
||||
(Lyons 1998), log-signatures, random reservoir projections, and the
|
||||
Salvi--Cass--Lyons signature kernel.
|
||||
|
||||
Truncated Signature
|
||||
-------------------
|
||||
|
||||
For a continuous path :math:`X : [0, T] \to \mathbb{R}^d` of bounded
|
||||
variation, the *signature* is the formal series
|
||||
|
||||
.. math::
|
||||
|
||||
S(X)_{0,T}
|
||||
\;=\;
|
||||
1 + \sum_{k \ge 1} \sum_{i_1, \dots, i_k}
|
||||
S^{i_1, \dots, i_k}_{0, T}\,
|
||||
e_{i_1} \otimes \dots \otimes e_{i_k},
|
||||
|
||||
with iterated Stieltjes integrals
|
||||
|
||||
.. math::
|
||||
|
||||
S^{i_1, \dots, i_k}_{0, T}
|
||||
\;=\;
|
||||
\int_{0 < u_1 < \dots < u_k < T}
|
||||
dX^{i_1}_{u_1}\, \dots\, dX^{i_k}_{u_k}.
|
||||
|
||||
For piecewise-linear input with increments :math:`\Delta_n`, the
|
||||
truncated signature obeys the multiplicative recursion
|
||||
|
||||
.. math::
|
||||
|
||||
S^{(M)}_{0, t_n}
|
||||
\;=\;
|
||||
S^{(M)}_{0, t_{n-1}}\,\otimes_M\,\exp_M(\Delta_n),
|
||||
|
||||
where :math:`\exp_M(\Delta) = \sum_{k=0}^M \Delta^{\otimes k} / k!`.
|
||||
|
||||
Log-Signature
|
||||
-------------
|
||||
|
||||
The truncated tensor logarithm
|
||||
|
||||
.. math::
|
||||
|
||||
\log(S)
|
||||
\;=\;
|
||||
\sum_{n \ge 1} \frac{(-1)^{n+1}}{n}\,(S - 1)^{\otimes n}
|
||||
|
||||
lives in the truncated free Lie algebra and provides a more
|
||||
parsimonious representation.
|
||||
|
||||
Random Signature
|
||||
----------------
|
||||
|
||||
Following Cuchiero--Schmocker--Teichmann (2023), one drives a random
|
||||
reservoir on :math:`\mathbb{R}^N`,
|
||||
|
||||
.. math::
|
||||
|
||||
dZ_t = A_0 Z_t\, dt + \sum_{i=1}^d A_i Z_t\, dX^i_t,
|
||||
|
||||
with random matrices :math:`A_i \in \mathbb{R}^{N \times N}` whose
|
||||
entries are i.i.d. Gaussian with variance :math:`1/N`. The map
|
||||
:math:`X \mapsto Z_T` is a finite-dimensional random projection of
|
||||
:math:`S(X)`.
|
||||
|
||||
Signature Kernel (Salvi--Cass--Lyons)
|
||||
-------------------------------------
|
||||
|
||||
The signature inner product
|
||||
|
||||
.. math::
|
||||
|
||||
K(s, t) \;=\; \langle S(X)_{0, s},\; S(Y)_{0, t}\rangle
|
||||
|
||||
solves the linear hyperbolic PDE
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{\partial^2 K}{\partial s\,\partial t}
|
||||
\;=\;
|
||||
\langle \dot X_s, \dot Y_t \rangle\, K(s, t),
|
||||
\qquad
|
||||
K(s, 0) = K(0, t) = 1.
|
||||
|
||||
It is integrated on a uniform grid via the Goursat scheme
|
||||
|
||||
.. math::
|
||||
|
||||
K_{i+1, j+1}
|
||||
= K_{i+1, j} + K_{i, j+1} - K_{i, j}
|
||||
+ \langle \Delta x_i, \Delta y_j\rangle\,
|
||||
\tfrac{1}{2}(K_{i+1, j} + K_{i, j+1}).
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub struct TruncatedSignature {
|
||||
pub channels: usize,
|
||||
pub level: usize,
|
||||
pub tensors: Vec<Vec<f64>>,
|
||||
}
|
||||
pub fn path_signature(path: &[Vec<f64>], level: usize) -> Result<TruncatedSignature>;
|
||||
pub fn log_signature(sig: &TruncatedSignature) -> Result<TruncatedLogSignature>;
|
||||
|
||||
pub struct RandomSignatureConfig {
|
||||
pub reservoir_dim: usize, pub seed: u64, pub variance: f64,
|
||||
}
|
||||
pub fn random_signature(path: &[Vec<f64>], cfg: &RandomSignatureConfig)
|
||||
-> Result<RandomSignatureResult>;
|
||||
|
||||
pub fn signature_kernel(x: &[Vec<f64>], y: &[Vec<f64>])
|
||||
-> Result<SignatureKernelResult>;
|
||||
@@ -0,0 +1,46 @@
|
||||
# Sparse Optimization
|
||||
|
||||
Sparse PCA, Elastic Net, and Box–Tao decomposition with Rust speed.
|
||||
|
||||
## Sparse PCA
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import sparse_pca_py
|
||||
|
||||
X = np.random.randn(500, 20)
|
||||
components = sparse_pca_py(X, n_components=5, l1_ratio=0.15)
|
||||
print(components.shape) # (5, 20)
|
||||
```
|
||||
|
||||
- Output: component matrix `(n_components, n_features)`; rows are sparse loadings.
|
||||
- Tuning: increase `l1_ratio` for harder sparsity; decrease to retain variance.
|
||||
|
||||
## Elastic Net
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import elastic_net_py
|
||||
|
||||
X = np.random.randn(200, 8)
|
||||
y = np.random.randn(200)
|
||||
coeffs = elastic_net_py(X, y, l1_ratio=0.3, alpha=0.01)
|
||||
print(coeffs)
|
||||
```
|
||||
|
||||
- Handles collinearity better than pure Lasso; use for factor shrinkage.
|
||||
- Sweep `alpha` on a log scale (e.g., $10^{-3}$ to $10^{-1}$) and pick via validation.
|
||||
|
||||
## Box–Tao decomposition
|
||||
|
||||
```python
|
||||
from optimizr import box_tao_decomposition_py
|
||||
solution = box_tao_decomposition_py(X)
|
||||
```
|
||||
|
||||
Useful for constrained sparse decomposition problems; the Rust backend keeps iterations fast.
|
||||
|
||||
## Practical notes
|
||||
- Inputs must be NumPy arrays; standardize features for stable conditioning.
|
||||
- For high dimensional data, start with fewer components/features to avoid over-regularization.
|
||||
- Combine with risk metrics: use sparse loadings to build interpretable factors, then evaluate with `compute_risk_metrics_py`.
|
||||
@@ -0,0 +1,67 @@
|
||||
Topological Data Analysis
|
||||
=========================
|
||||
|
||||
The module :code:`topology` implements Vietoris--Rips persistent
|
||||
homology and the bottleneck distance between persistence diagrams.
|
||||
|
||||
Vietoris--Rips Filtration
|
||||
-------------------------
|
||||
|
||||
For a finite point cloud :math:`X = \{x_1, \dots, x_n\} \subset \mathbb{R}^d`
|
||||
and scale :math:`\varepsilon \ge 0`, the Vietoris--Rips complex is
|
||||
|
||||
.. math::
|
||||
|
||||
\mathrm{VR}_\varepsilon(X)
|
||||
\;=\;
|
||||
\big\{ \sigma \subseteq X : \mathrm{diam}(\sigma) \le \varepsilon \big\}.
|
||||
|
||||
Increasing :math:`\varepsilon` yields a filtration; the persistent
|
||||
homology of this filtration produces, for each homological degree
|
||||
:math:`k`, a multiset of birth/death pairs
|
||||
|
||||
.. math::
|
||||
|
||||
D_k(X) = \big\{ (b_i, d_i) : 0 \le b_i < d_i \le \infty \big\}.
|
||||
|
||||
Persistence Algorithm
|
||||
---------------------
|
||||
|
||||
The boundary matrix :math:`\partial` is built over :math:`\mathbb{Z}/2`
|
||||
and reduced left-to-right: for each column :math:`j` we cancel its
|
||||
lowest entry by adding any earlier column with the same low. Pairs
|
||||
:math:`(\mathrm{low}(j), j)` give birth/death pairs.
|
||||
|
||||
Bottleneck Distance
|
||||
-------------------
|
||||
|
||||
For two diagrams :math:`D` and :math:`D'`,
|
||||
|
||||
.. math::
|
||||
|
||||
d_B(D, D')
|
||||
\;=\;
|
||||
\inf_{\eta : D \to D'}\;
|
||||
\sup_{x \in D}\, \|x - \eta(x)\|_\infty,
|
||||
|
||||
where matchings may pair points with the diagonal
|
||||
:math:`\Delta = \{(t, t) : t \ge 0\}` at cost :math:`(d - b)/2`.
|
||||
|
||||
The implementation binary-searches the threshold :math:`\varepsilon`
|
||||
and certifies a perfect matching by Hopcroft--Karp on the bipartite
|
||||
graph of admissible edges.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub struct PersistencePair { pub dim: usize, pub birth: f64, pub death: f64 }
|
||||
pub struct PersistenceDiagram { pub pairs: Vec<PersistencePair> }
|
||||
|
||||
pub fn vietoris_rips_filtration(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
|
||||
-> Result<Vec<Simplex>>;
|
||||
pub fn persistent_homology(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
|
||||
-> Result<PersistenceDiagram>;
|
||||
pub fn bottleneck_distance(d1: &[PersistencePair], d2: &[PersistencePair])
|
||||
-> Result<f64>;
|
||||
@@ -0,0 +1,122 @@
|
||||
Volterra and Fractional Solvers
|
||||
================================
|
||||
|
||||
The module :code:`volterra` collects four CPU-only generic numerical
|
||||
primitives for Volterra integral equations and related transforms.
|
||||
|
||||
Fractional Caputo Adams Solver
|
||||
------------------------------
|
||||
|
||||
For :math:`\alpha \in (0, 1)`, solve
|
||||
|
||||
.. math::
|
||||
|
||||
D^\alpha h(t) = F(t, h(t)),
|
||||
\qquad h(0) = h_0,
|
||||
|
||||
with the Diethelm--Ford--Freed (2002) fractional Adams predictor--
|
||||
corrector. Predictor:
|
||||
|
||||
.. math::
|
||||
|
||||
h^P_{n+1}
|
||||
= h_0
|
||||
+ \frac{\Delta t^\alpha}{\alpha\,\Gamma(\alpha)}
|
||||
\sum_{k=0}^{n}
|
||||
\big[(n+1-k)^\alpha - (n-k)^\alpha\big]\, F(t_k, h_k).
|
||||
|
||||
Corrector:
|
||||
|
||||
.. math::
|
||||
|
||||
h_{n+1}
|
||||
= h_0
|
||||
+ \frac{\Delta t^\alpha}{\Gamma(\alpha + 2)}
|
||||
\Big[ F(t_{n+1}, h^P_{n+1}) + \sum_{k=0}^{n} a_{n+1, k}\, F(t_k, h_k) \Big],
|
||||
|
||||
with
|
||||
|
||||
.. math::
|
||||
|
||||
a_{n+1, 0} = n^{\alpha + 1} - (n - \alpha)\,(n+1)^\alpha,
|
||||
|
||||
a_{n+1, k} = (n - k + 2)^{\alpha + 1} + (n - k)^{\alpha + 1}
|
||||
- 2\,(n - k + 1)^{\alpha + 1},
|
||||
\qquad 1 \le k \le n.
|
||||
|
||||
Markovian Lift
|
||||
--------------
|
||||
|
||||
A convolution kernel :math:`K(t)` admitting
|
||||
|
||||
.. math::
|
||||
|
||||
K(t) = \int_0^\infty e^{-\gamma t}\, \nu(d\gamma)
|
||||
|
||||
is approximated by
|
||||
|
||||
.. math::
|
||||
|
||||
K(t) \;\approx\; \sum_{j=1}^N c_j\, e^{-\gamma_j t},
|
||||
\qquad c_j \ge 0,
|
||||
|
||||
with rates :math:`\gamma_j` on a geometric grid and weights fitted by
|
||||
non-negative least squares.
|
||||
|
||||
Generic Volterra Equation
|
||||
-------------------------
|
||||
|
||||
For
|
||||
|
||||
.. math::
|
||||
|
||||
y(t) = g(t) + \int_0^t K(t - s, y(s))\, ds,
|
||||
|
||||
the trapezoidal product-integration scheme reads
|
||||
|
||||
.. math::
|
||||
|
||||
y_n = g_n + \Delta t\,\Big[
|
||||
\tfrac{1}{2} K(t_n, y_0)
|
||||
+ \sum_{k=1}^{n-1} K(t_n - t_k, y_k)
|
||||
+ \tfrac{1}{2} K(0, y_n) \Big],
|
||||
|
||||
solved implicitly by fixed-point iteration on :math:`y_n`.
|
||||
|
||||
Fourier Inversion
|
||||
-----------------
|
||||
|
||||
Recover a density from a characteristic function :math:`\varphi(u)` via
|
||||
|
||||
.. math::
|
||||
|
||||
f(x) \;\approx\;
|
||||
\frac{\Delta u}{\pi}\,
|
||||
\sum_{k=0}^{N_u - 1}
|
||||
w_k \big[\,\Re \varphi(u_k)\,\cos(u_k x)
|
||||
+ \Im \varphi(u_k)\,\sin(u_k x)\,\big],
|
||||
|
||||
with trapezoidal weights :math:`w_k`.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub fn solve_fractional_ode<F: Fn(f64, f64) -> f64>(
|
||||
h0: f64, alpha: f64, t_horizon: f64, n_steps: usize, rhs: F,
|
||||
) -> Result<FractionalOdeResult>;
|
||||
|
||||
pub fn geometric_grid_lift<K: Fn(f64) -> f64>(
|
||||
kernel: K, t_samples: &[f64],
|
||||
n_factors: usize, gamma_min: f64, gamma_max: f64, nnls_iter: usize,
|
||||
) -> Result<MarkovianLift>;
|
||||
|
||||
pub fn solve_volterra<G, K>(
|
||||
g: G, kernel: K, t_horizon: f64, n_steps: usize,
|
||||
fixed_point_iter: usize, fixed_point_tol: f64,
|
||||
) -> Result<VolterraResult>;
|
||||
|
||||
pub fn fourier_invert<P: Fn(f64) -> (f64, f64)>(
|
||||
phi: P, x_grid: &[f64], u_max: f64, n_u: usize,
|
||||
) -> Result<DensityResult>;
|
||||
@@ -0,0 +1,51 @@
|
||||
Discrete and Maximum-Overlap Wavelet Transforms
|
||||
================================================
|
||||
|
||||
The module :code:`timeseries_utils::wavelet` provides Haar and Daubechies
|
||||
wavelet transforms with periodic boundary handling.
|
||||
|
||||
Filter banks
|
||||
------------
|
||||
|
||||
For an orthogonal scaling filter :math:`\{h_k\}_{k=0}^{L-1}` the quadrature
|
||||
mirror filter (QMF) is
|
||||
|
||||
.. math::
|
||||
|
||||
g_k = (-1)^k\, h_{L - 1 - k},
|
||||
|
||||
so that :math:`\sum_k h_k = \sqrt{2}` and :math:`\sum_k g_k = 0`.
|
||||
|
||||
DWT (one level, periodic)
|
||||
-------------------------
|
||||
|
||||
For an input vector :math:`x \in \mathbb{R}^N` with :math:`N` even,
|
||||
|
||||
.. math::
|
||||
|
||||
a_n = \sum_{k=0}^{L-1} h_k\, x_{(2n + k)\bmod N},
|
||||
\qquad
|
||||
d_n = \sum_{k=0}^{L-1} g_k\, x_{(2n + k)\bmod N},
|
||||
\qquad n = 0, \dots, N/2 - 1.
|
||||
|
||||
Successive levels apply the same filter to the previous approximation
|
||||
:math:`a^{(j)}`.
|
||||
|
||||
MODWT (Maximum Overlap)
|
||||
-----------------------
|
||||
|
||||
The MODWT does not downsample: at level :math:`j`, the filter is dilated
|
||||
by inserting :math:`2^{j-1} - 1` zeros between successive taps and applied
|
||||
in a periodic convolution. The result is shift-invariant.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub enum WaveletFamily { Haar, Daubechies(u8) }
|
||||
pub fn scaling_filter(family: WaveletFamily) -> Result<Vec<f64>>;
|
||||
pub fn qmf(h: &[f64]) -> Vec<f64>;
|
||||
pub fn dwt_step(x: &[f64], h: &[f64], g: &[f64]) -> (Vec<f64>, Vec<f64>);
|
||||
pub fn dwt(x: &[f64], family: WaveletFamily, levels: usize) -> Result<Vec<Vec<f64>>>;
|
||||
pub fn modwt_step(x: &[f64], h: &[f64], g: &[f64], level: usize) -> (Vec<f64>, Vec<f64>);
|
||||
@@ -0,0 +1,45 @@
|
||||
# API: differential_evolution
|
||||
|
||||
```python
|
||||
from optimizr import differential_evolution
|
||||
|
||||
best_x, best_fx = differential_evolution(
|
||||
objective_fn,
|
||||
bounds,
|
||||
popsize=15,
|
||||
maxiter=1000,
|
||||
f=None, # mutation factor (auto if None)
|
||||
cr=None, # crossover rate (auto if None)
|
||||
strategy="rand1", # rand1, best1, currenttobest1, rand2, best2
|
||||
seed=None,
|
||||
tol=1e-6,
|
||||
atol=1e-8,
|
||||
track_history=False, # keep per-iter best
|
||||
parallel=False, # Python callbacks stay sequential; see Rust path below
|
||||
adaptive=False, # jDE when True
|
||||
constraint_penalty=1000.0,
|
||||
)
|
||||
```
|
||||
|
||||
- `objective_fn`: callable `f(x: np.ndarray) -> float`
|
||||
- `bounds`: list of `(min, max)` tuples
|
||||
- Returns `(best_x: np.ndarray, best_fx: float)`
|
||||
|
||||
## Parallel Rust entry point
|
||||
|
||||
For built-in benchmark objectives (no Python callbacks), use the Rust-native path with Rayon:
|
||||
|
||||
```python
|
||||
from optimizr import parallel_differential_evolution_rust
|
||||
|
||||
result = parallel_differential_evolution_rust(
|
||||
objective_name="rastrigin", # sphere, rosenbrock, ackley, griewank
|
||||
bounds=[(-5, 5)] * 20,
|
||||
maxiter=500,
|
||||
parallel=True,
|
||||
)
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Adaptive control uses jDE in the current Python API; SHADE/L-SHADE live in Rust and will surface in a future release.
|
||||
- Use `track_history=True` to export convergence curves for benchmarking.
|
||||
@@ -0,0 +1,14 @@
|
||||
# API: grid_search
|
||||
|
||||
```python
|
||||
from optimizr import grid_search
|
||||
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn,
|
||||
param_grid,
|
||||
)
|
||||
```
|
||||
|
||||
- `objective_fn`: callable receiving a dict of parameters and returning a scalar loss
|
||||
- `param_grid`: dict of name -> list of values to enumerate
|
||||
- Returns `(best_params: dict, best_score: float)`
|
||||
@@ -0,0 +1,571 @@
|
||||
# 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)
|
||||
|
||||
# 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]}")
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,16 @@
|
||||
# API: HMM
|
||||
|
||||
```python
|
||||
from optimizr import HMM
|
||||
|
||||
model = HMM(n_states=2)
|
||||
model.fit(X, n_iterations=100, tolerance=1e-6)
|
||||
states = model.predict(X)
|
||||
logp = model.score(X)
|
||||
```
|
||||
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
# API: mcmc_sample
|
||||
|
||||
```python
|
||||
from optimizr import mcmc_sample
|
||||
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn,
|
||||
data,
|
||||
initial_params,
|
||||
param_bounds,
|
||||
n_samples=10000,
|
||||
burn_in=1000,
|
||||
proposal_std=0.1,
|
||||
)
|
||||
```
|
||||
|
||||
- `log_likelihood_fn(params, data) -> float`
|
||||
- `data`: np.ndarray passed through to the likelihood
|
||||
- `initial_params`: np.ndarray starting point
|
||||
- `param_bounds`: list of `(min, max)` tuples
|
||||
- Returns `samples: np.ndarray` of shape `(n_samples, n_params)`
|
||||
@@ -0,0 +1,117 @@
|
||||
# API: Optimal Control / Kalman
|
||||
|
||||
High-level bindings exposed by the `optimizr` Python package. All functions require the Rust extension (`optimizr._core`).
|
||||
|
||||
**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
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## OU parameter estimation
|
||||
|
||||
```python
|
||||
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})$.
|
||||
|
||||
**Practical guidance:** Use at least a few thousand samples for stable estimates; heavy-tailed series benefit from pre-whitening or winsorizing before fitting.
|
||||
|
||||
## 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.
|
||||
|
||||
**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
|
||||
|
||||
## 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]],
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Batch filtering
|
||||
result = kf.filter(observations=[[1.0], [1.4], [1.9]], controls=None)
|
||||
states = result.get_states()
|
||||
log_likelihoods = result.get_log_likelihoods()
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,117 @@
|
||||
# API: Optimal Control / Kalman
|
||||
|
||||
High-level bindings exposed by the `optimizr` Python package. All functions require the Rust extension (`optimizr._core`).
|
||||
|
||||
**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
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
## OU parameter estimation
|
||||
|
||||
```python
|
||||
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})$.
|
||||
|
||||
**Practical guidance:** Use at least a few thousand samples for stable estimates; heavy-tailed series benefit from pre-whitening or winsorizing before fitting.
|
||||
|
||||
## 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.
|
||||
|
||||
**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
|
||||
|
||||
## 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]],
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Batch filtering
|
||||
result = kf.filter(observations=[[1.0], [1.4], [1.9]], controls=None)
|
||||
states = result.get_states()
|
||||
log_likelihoods = result.get_log_likelihoods()
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,450 @@
|
||||
# API Reference: Point Processes
|
||||
|
||||
The point processes module provides Rust-accelerated functions for Hawkes process simulation, fractional Brownian motion, and related special functions — all accessible from Python via PyO3.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Simulate a Hawkes process with power-law kernel
|
||||
events = optimizr.simulate_hawkes(
|
||||
baseline=0.1,
|
||||
alpha=0.35,
|
||||
beta=0.375,
|
||||
t_max=100.0,
|
||||
kernel_type="power_law",
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Simulate fractional Brownian motion
|
||||
path = optimizr.simulate_fbm(hurst=0.75, n=1000, dt=0.01, seed=42)
|
||||
|
||||
# Estimate Hurst exponent
|
||||
h_est = optimizr.estimate_hurst(path)
|
||||
print(f"Estimated H: {h_est:.3f}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hawkes Process Functions
|
||||
|
||||
### `simulate_hawkes(baseline, alpha, beta, t_max, kernel_type="exponential", seed=None)`
|
||||
|
||||
Simulate a univariate Hawkes process using Ogata's thinning algorithm.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `baseline` (float): Baseline intensity $\nu > 0$. This is the exogenous event rate in the absence of self-excitation. Higher values produce more events even without clustering.
|
||||
|
||||
- `alpha` (float): Kernel amplitude parameter.
|
||||
- For `"exponential"`: peak excitation rate $\alpha$ in $\phi(t) = \alpha e^{-\beta t}$
|
||||
- For `"power_law"`: scaling constant $K_0$ in $\phi(t) = K_0 (1+t)^{-(1+\alpha_0)}$
|
||||
|
||||
- `beta` (float): Kernel decay parameter.
|
||||
- For `"exponential"`: decay rate $\beta$ (inverse timescale)
|
||||
- For `"power_law"`: tail exponent $\alpha_0 \in (0, 1)$. Connected to Hurst parameter by $H_0 = 2\alpha_0$.
|
||||
|
||||
- `t_max` (float): Maximum simulation time $T$. The process runs on $[0, T]$.
|
||||
|
||||
- `kernel_type` (str, default=`"exponential"`): Type of excitation kernel.
|
||||
- `"exponential"`: Short-memory kernel with exponential decay
|
||||
- `"power_law"`: Long-memory kernel with power-law tail
|
||||
|
||||
- `seed` (int, optional): Random seed for reproducibility.
|
||||
|
||||
**Returns:**
|
||||
- `np.ndarray`: Array of event times $\{t_1, t_2, \ldots, t_n\}$ sorted in ascending order.
|
||||
|
||||
**Stability:**
|
||||
- Exponential: stable when $\alpha / \beta < 1$
|
||||
- Power-law: stable when $K_0 / \alpha_0 < 1$
|
||||
|
||||
**Examples:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
|
||||
# Markovian self-exciting process (exponential kernel)
|
||||
events = optimizr.simulate_hawkes(
|
||||
baseline=1.0,
|
||||
alpha=0.5, # branching ratio = 0.5/1.0 = 0.5
|
||||
beta=1.0,
|
||||
t_max=100.0,
|
||||
kernel_type="exponential",
|
||||
seed=42
|
||||
)
|
||||
print(f"{len(events)} events, expected ≈ {1.0 / (1 - 0.5) * 100:.0f}")
|
||||
|
||||
# Long-memory process (power-law kernel, H₀ = 0.75)
|
||||
events_pl = optimizr.simulate_hawkes(
|
||||
baseline=0.1,
|
||||
alpha=0.35, # K₀
|
||||
beta=0.375, # α₀ → H₀ = 0.75
|
||||
t_max=1000.0,
|
||||
kernel_type="power_law",
|
||||
seed=42
|
||||
)
|
||||
print(f"{len(events_pl)} events with long-memory clustering")
|
||||
```
|
||||
|
||||
**When to use which kernel:**
|
||||
|
||||
| Scenario | Kernel | Typical Parameters |
|
||||
|----------|--------|--------------------|
|
||||
| High-frequency order arrivals | Exponential | $\alpha=0.5$, $\beta=2.0$ |
|
||||
| Market microstructure (unified theory) | Power-law | $\alpha_0=0.375$, $K_0 \leq \alpha_0$ |
|
||||
| Neural spike trains | Exponential | $\alpha=0.3$, $\beta=5.0$ |
|
||||
| Seismology (aftershocks) | Power-law | $\alpha_0=0.5$, $K_0=0.4$ |
|
||||
|
||||
---
|
||||
|
||||
### `simulate_bivariate_hawkes(core_buy_times, core_sell_times, phi1_alpha, phi1_beta, phi2_alpha, phi2_beta, t_max, seed=None)`
|
||||
|
||||
Simulate a bivariate Hawkes process modeling buy/sell reaction order flow driven by core order flow.
|
||||
|
||||
The model captures how buy orders excite more buy orders (**self-excitation**) and sell orders (**cross-excitation**), and vice versa.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `core_buy_times` (np.ndarray): Core buy order arrival times (driver process $F^+$).
|
||||
- `core_sell_times` (np.ndarray): Core sell order arrival times (driver process $F^-$).
|
||||
- `phi1_alpha` (float): Self-excitation kernel amplitude (buy→buy, sell→sell).
|
||||
- `phi1_beta` (float): Self-excitation kernel decay rate.
|
||||
- `phi2_alpha` (float): Cross-excitation kernel amplitude (buy→sell, sell→buy).
|
||||
- `phi2_beta` (float): Cross-excitation kernel decay rate.
|
||||
- `t_max` (float): Maximum simulation time.
|
||||
- `seed` (int, optional): Random seed.
|
||||
|
||||
**Returns:**
|
||||
- `tuple[np.ndarray, np.ndarray]`: `(buy_times, sell_times)` — arrays of reaction buy and sell event times.
|
||||
|
||||
**Stability condition:**
|
||||
|
||||
$$
|
||||
\frac{\phi_{1,\alpha}}{\phi_{1,\beta}} + \frac{\phi_{2,\alpha}}{\phi_{2,\beta}} < 1
|
||||
$$
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Core flow: Poisson arrivals
|
||||
rng = np.random.default_rng(42)
|
||||
core_buys = np.sort(rng.uniform(0, 100, 200))
|
||||
core_sells = np.sort(rng.uniform(0, 100, 180))
|
||||
|
||||
# Symmetric reaction with moderate cross-excitation
|
||||
buys, sells = optimizr.simulate_bivariate_hawkes(
|
||||
core_buy_times=core_buys,
|
||||
core_sell_times=core_sells,
|
||||
phi1_alpha=0.3, # Self: L¹ = 0.3
|
||||
phi1_beta=1.0,
|
||||
phi2_alpha=0.15, # Cross: L¹ = 0.15
|
||||
phi2_beta=1.0,
|
||||
t_max=100.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Net order imbalance (price signal)
|
||||
imbalance = len(buys) - len(sells)
|
||||
print(f"Reaction buys: {len(buys)}, sells: {len(sells)}")
|
||||
print(f"Order imbalance: {imbalance:+d}")
|
||||
print(f"Spectral radius: {0.3 + 0.15:.2f}") # 0.45 < 1 → stable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fractional Brownian Motion Functions
|
||||
|
||||
### `simulate_fbm(hurst, n, dt=1.0, seed=None)`
|
||||
|
||||
Simulate a fractional Brownian motion sample path using Hosking's method (Durbin-Levinson algorithm).
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `hurst` (float): Hurst parameter $H \in (0, 1)$.
|
||||
- $H < 0.5$: Anti-persistent (mean-reverting)
|
||||
- $H = 0.5$: Standard Brownian motion
|
||||
- $H > 0.5$: Persistent (trending)
|
||||
|
||||
- `n` (int): Number of time steps. The output has $n + 1$ values (including $B^H_0 = 0$).
|
||||
|
||||
- `dt` (float, default=1.0): Time step size $\Delta t$. Increments are scaled by $(\Delta t)^H$.
|
||||
|
||||
- `seed` (int, optional): Random seed.
|
||||
|
||||
**Returns:**
|
||||
- `np.ndarray`: Array of length $n + 1$ representing the fBM path $\{B^H_0, B^H_{\Delta t}, B^H_{2\Delta t}, \ldots, B^H_{n\Delta t}\}$.
|
||||
|
||||
**Complexity:** $O(n^2)$ using Hosking's method (vs $O(n^3)$ for Cholesky).
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Compare three regimes
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
||||
|
||||
for i, (h, label) in enumerate([
|
||||
(0.3, "Anti-persistent"),
|
||||
(0.5, "Standard BM"),
|
||||
(0.8, "Persistent")
|
||||
]):
|
||||
path = optimizr.simulate_fbm(hurst=h, n=2000, dt=0.001, seed=42)
|
||||
axes[i].plot(path, linewidth=0.5, color=['red', 'black', 'blue'][i])
|
||||
axes[i].set_title(f"H = {h} ({label})")
|
||||
axes[i].set_xlabel("Time step")
|
||||
axes[i].set_ylabel("B^H(t)")
|
||||
|
||||
plt.suptitle("Fractional Brownian Motion: Three Regimes")
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `simulate_mixed_fbm(a, b, hurst, n, dt=1.0, seed=None)`
|
||||
|
||||
Simulate a mixed fractional Brownian motion $M^H(t) = a \cdot B(t) + b \cdot B^H(t)$.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `a` (float): Coefficient for the standard BM component (diffusive).
|
||||
- `b` (float): Coefficient for the fBM component (persistent).
|
||||
- `hurst` (float): Hurst parameter $H$ of the fBM component. Typically $H \in (0.5, 1)$ for persistent flow.
|
||||
- `n` (int): Number of time steps.
|
||||
- `dt` (float, default=1.0): Time step size.
|
||||
- `seed` (int, optional): Random seed.
|
||||
|
||||
**Returns:**
|
||||
- `np.ndarray`: Array of length $n + 1$ representing the mixed fBM path.
|
||||
|
||||
**Financial interpretation:**
|
||||
- The BM component captures short-term noise (market making, latency)
|
||||
- The fBM component captures long-term persistence (informed trading, herding)
|
||||
- At short timescales, $H_{\text{eff}} \to 1/2$ (BM dominates)
|
||||
- At long timescales, $H_{\text{eff}} \to H$ (fBM dominates)
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Unified theory: aggregate order flow as mfBM
|
||||
path = optimizr.simulate_mixed_fbm(
|
||||
a=1.0, # BM weight
|
||||
b=0.5, # fBM weight
|
||||
hurst=0.75, # H₀ from unified theory
|
||||
n=5000,
|
||||
dt=0.01,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Semimartingale check: H > 3/4 allows classical stochastic calculus
|
||||
is_semimartingale = 0.75 > 0.75 # Borderline case
|
||||
print(f"Path length: {len(path)}")
|
||||
print(f"Semimartingale: {is_semimartingale}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `estimate_hurst(data)`
|
||||
|
||||
Estimate the Hurst exponent from data using Rescaled Range (R/S) analysis.
|
||||
|
||||
**Parameters:**
|
||||
- `data` (np.ndarray): 1-D time series data (path values, not increments).
|
||||
|
||||
**Returns:**
|
||||
- `float`: Estimated Hurst exponent $\hat{H} \in [0.01, 0.99]$.
|
||||
|
||||
**Algorithm:**
|
||||
1. Partition data into subseries of varying lengths $n_1, n_2, \ldots$
|
||||
2. For each length, compute the average R/S statistic across subseries
|
||||
3. Fit $\log(R/S) = H \log(n) + c$ by least-squares regression
|
||||
|
||||
**Note:** R/S analysis provides a rough estimate. For more precise estimation, consider DFA (Detrended Fluctuation Analysis) or wavelet methods. The method requires at least 20 data points.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Verify estimation accuracy
|
||||
for h_true in [0.3, 0.5, 0.7, 0.9]:
|
||||
path = optimizr.simulate_fbm(hurst=h_true, n=5000, dt=1.0, seed=42)
|
||||
h_est = optimizr.estimate_hurst(path)
|
||||
print(f"H_true = {h_true:.1f}, H_est = {h_est:.3f}, error = {abs(h_true - h_est):.3f}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `scale_dependent_hurst(data, scales=None)`
|
||||
|
||||
Compute scale-dependent Hurst exponents using variance ratios at different time scales.
|
||||
|
||||
This function identifies whether data follows a pure fBM or a mixed fBM by examining how the effective Hurst exponent varies across scales.
|
||||
|
||||
**Parameters:**
|
||||
- `data` (np.ndarray): 1-D time series data.
|
||||
- `scales` (list of int, optional): Time scales to analyze. Default: `[10, 50, 100, 500, 1000, 2000, 5000]`.
|
||||
|
||||
**Returns:**
|
||||
- `dict[int, float]`: Mapping from scale to estimated Hurst exponent at that scale.
|
||||
|
||||
**Interpretation:**
|
||||
- **Constant $H(\Delta)$** across scales → pure fBM
|
||||
- **$H(\Delta)$ increasing from $\sim 0.5$ to $H$** → mixed fBM (BM at short scales, fBM at long scales)
|
||||
- **$H(\Delta) \approx 0.5$** at all scales → standard BM (no long memory)
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Generate mixed fBM (should show scale-dependent H)
|
||||
path = optimizr.simulate_mixed_fbm(a=1.0, b=1.0, hurst=0.8, n=10000, dt=1.0, seed=42)
|
||||
|
||||
hurst_scales = optimizr.scale_dependent_hurst(
|
||||
data=path,
|
||||
scales=[10, 25, 50, 100, 250, 500, 1000, 2500]
|
||||
)
|
||||
|
||||
print("Scale | H_effective")
|
||||
print("-" * 25)
|
||||
for s, h in sorted(hurst_scales.items()):
|
||||
indicator = "← BM regime" if h < 0.55 else ("← fBM regime" if h > 0.65 else "← transition")
|
||||
print(f"{s:>5d} | {h:.4f} {indicator}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Special Functions
|
||||
|
||||
### `mittag_leffler_py(alpha, beta, z)`
|
||||
|
||||
Compute the generalized Mittag-Leffler function $E_{\alpha,\beta}(z)$.
|
||||
|
||||
**Parameters:**
|
||||
- `alpha` (float): First parameter $\alpha > 0$.
|
||||
- `beta` (float): Second parameter $\beta > 0$.
|
||||
- `z` (float): Real argument.
|
||||
|
||||
**Returns:**
|
||||
- `float`: $E_{\alpha,\beta}(z)$
|
||||
|
||||
**Algorithm:**
|
||||
- $|z| < 10$: Taylor series expansion with 100 terms
|
||||
- $|z| \geq 10$: Asymptotic expansion
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
|
||||
# Verify: E_{1,1}(z) = exp(z)
|
||||
for z in [0.5, 1.0, 2.0]:
|
||||
ml = optimizr.mittag_leffler_py(1.0, 1.0, z)
|
||||
print(f"E_{{1,1}}({z}) = {ml:.8f}, exp({z}) = {np.exp(z):.8f}")
|
||||
|
||||
# Compute E_{0.5, 1}(z) (related to complementary error function)
|
||||
z = -1.0
|
||||
ml_half = optimizr.mittag_leffler_py(0.5, 1.0, z)
|
||||
print(f"E_{{0.5,1}}({z}) = {ml_half:.8f}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `f_alpha_lambda_py(alpha0, lambda0, x)`
|
||||
|
||||
Compute the scaling function $f_{\alpha_0, \lambda_0}(x)$ from Theorem 3.1.
|
||||
|
||||
$$
|
||||
f_{\alpha_0, \lambda_0}(x) = \lambda_0 \, x^{\alpha_0 - 1} \, E_{\alpha_0, \alpha_0}\!\left(-\lambda_0 \, x^{\alpha_0}\right)
|
||||
$$
|
||||
|
||||
**Parameters:**
|
||||
- `alpha0` (float): Tail exponent $\alpha_0 \in (0, 1)$.
|
||||
- `lambda0` (float): Scaling parameter $\lambda_0 > 0$.
|
||||
- `x` (float): Evaluation point $x > 0$.
|
||||
|
||||
**Returns:**
|
||||
- `float`: $f_{\alpha_0, \lambda_0}(x)$
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Plot for H₀ = 0.75 → α₀ = 0.375
|
||||
x = np.linspace(0.01, 20, 500)
|
||||
f = [optimizr.f_alpha_lambda_py(0.375, 1.0, xi) for xi in x]
|
||||
|
||||
plt.figure(figsize=(8, 4))
|
||||
plt.plot(x, f)
|
||||
plt.xlabel('x')
|
||||
plt.ylabel(r'$f_{0.375, 1.0}(x)$')
|
||||
plt.title('Scaling Function (Theorem 3.1)')
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.show()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Architecture
|
||||
|
||||
```{mermaid}
|
||||
graph TD
|
||||
MOD["📦 mod.rs\nPublic API re-exports"]
|
||||
K["🔧 kernels.rs\nExcitationKernel trait"]
|
||||
H["⚡ hawkes.rs\nSimulation & fitting"]
|
||||
ML["🔢 mittag_leffler.rs\nSpecial functions"]
|
||||
FBM["〰️ mixed_fbm.rs\nFractional Brownian motion"]
|
||||
PY["🐍 python_bindings.rs\nPyO3 bindings"]
|
||||
|
||||
MOD --> K
|
||||
MOD --> H
|
||||
MOD --> ML
|
||||
MOD --> FBM
|
||||
MOD --> PY
|
||||
|
||||
K --> EK["ExponentialKernel\nφ = α·e^−βt"]
|
||||
K --> PLK["PowerLawKernel\nφ = K₀(1+t)^−1−α"]
|
||||
K --> CMK["CompletelyMonotoneKernel\nMittag-Leffler"]
|
||||
|
||||
H --> HP["HawkesProcess K\nunivariate · Ogata thinning"]
|
||||
H --> BH["BivariateHawkes K\nbuy/sell reaction flow"]
|
||||
|
||||
ML --> mf["mittag_leffler · f_alpha_lambda\ngamma · incomplete_gamma"]
|
||||
|
||||
FBM --> fbm1["FractionalBM\nCholesky & Hosking"]
|
||||
FBM --> fbm2["MixedFractionalBM\na·B + b·B^H"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
All computations run in Rust, providing significant speedups over pure Python:
|
||||
|
||||
| Operation | n | Rust (optimizr) | Python (pure) | Speedup |
|
||||
|-----------|---|-----------------|---------------|---------|
|
||||
| Hawkes simulation (exp) | 10K events | ~2ms | ~150ms | **75×** |
|
||||
| fBM (Hosking) | 5000 steps | ~15ms | ~800ms | **53×** |
|
||||
| Hurst estimation (R/S) | 10K points | ~1ms | ~60ms | **60×** |
|
||||
| Mittag-Leffler | 100 terms | ~5μs | ~300μs | **60×** |
|
||||
|
||||
Benchmarks on Apple M1 Pro, single thread. The Hawkes simulation uses Ogata's thinning which depends on the branching ratio — higher branching ratios (closer to 1) produce more events and take longer.
|
||||
@@ -0,0 +1,19 @@
|
||||
# API: Risk Metrics
|
||||
|
||||
```python
|
||||
from optimizr import (
|
||||
hurst_exponent_py,
|
||||
compute_risk_metrics_py,
|
||||
estimate_half_life_py,
|
||||
bootstrap_returns_py,
|
||||
)
|
||||
|
||||
h = hurst_exponent_py(returns)
|
||||
hl = estimate_half_life_py(returns)
|
||||
metrics = compute_risk_metrics_py(returns.tolist())
|
||||
boot = bootstrap_returns_py(returns, n_samples=1000)
|
||||
```
|
||||
|
||||
- `returns`: 1D NumPy array of returns
|
||||
- `compute_risk_metrics_py` returns a dict with volatility, Sharpe, and drawdown estimates
|
||||
- `bootstrap_returns_py` resamples the series for uncertainty estimation
|
||||
@@ -0,0 +1,26 @@
|
||||
# API: Sparse Optimization
|
||||
|
||||
## sparse_pca_py
|
||||
```python
|
||||
from optimizr import sparse_pca_py
|
||||
|
||||
components = sparse_pca_py(
|
||||
X,
|
||||
n_components=3,
|
||||
l1_ratio=0.2,
|
||||
)
|
||||
```
|
||||
- `X`: 2D NumPy array
|
||||
- Returns component matrix `(n_components, n_features)`
|
||||
|
||||
## box_tao_decomposition_py
|
||||
```python
|
||||
from optimizr import box_tao_decomposition_py
|
||||
solution = box_tao_decomposition_py(X)
|
||||
```
|
||||
|
||||
## elastic_net_py
|
||||
```python
|
||||
from optimizr import elastic_net_py
|
||||
coeffs = elastic_net_py(X, y, l1_ratio=0.3, alpha=0.01)
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Benchmarks
|
||||
|
||||
These results come from the Rust backends (release build) versus SciPy’s `differential_evolution` on the standard 10D test suite. Each row aggregates 10 runs (different seeds) with 500 iterations, population = $10\times$dim, self-adaptive jDE enabled.
|
||||
|
||||
| Function | Dim | Iterations | Success Rate | Avg Time (Rust) | Best Fitness | Speedup vs SciPy |
|
||||
|----------|-----|------------|--------------|-----------------|--------------|------------------|
|
||||
| Sphere | 10 | 500 | 100% | 12 ms | $1\times10^{-12}$ | 70× |
|
||||
| Rosenbrock | 10 | 500 | 98% | 18 ms | $3\times10^{-6}$ | 65× |
|
||||
| Rastrigin | 10 | 500 | 87% | 22 ms | $2\times10^{-2}$ | 72× |
|
||||
| Ackley | 10 | 500 | 95% | 15 ms | $2\times10^{-8}$ | 58× |
|
||||
|
||||
**How to reproduce**
|
||||
|
||||
- Run [`examples/notebooks/05_performance_benchmarks.ipynb`](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/05_performance_benchmarks.ipynb) (validated in CI) to regenerate figures and raw CSV metrics.
|
||||
- Or from the [repo root](https://github.com/ThotDjehuty/optimiz-r), run `make benchmark` for the Rust-side microbenchmarks (no Python overhead).
|
||||
- To compare against SciPy, set `SCIPY_BASELINE=1` in the notebook; it records wall-clock times and success percentages side by side.
|
||||
|
||||
**What the notebook plots**
|
||||
|
||||
- Convergence trajectories (best fitness vs iterations) for each function
|
||||
- Histograms of self-adapted $(F, CR)$ values mid-run
|
||||
- Speedup bars and success-rate bars vs SciPy on the same seeds
|
||||
- Residuals heatmap for a sweep over population sizes (optional cell)
|
||||
|
||||
**Notes on methodology**
|
||||
|
||||
- Rust builds are compiled with `--release` and link against OpenBLAS.
|
||||
- Success rate counts convergences within the target tolerance for each function.
|
||||
- Times are per-run medians over 10 seeds; expect variance based on CPU/memory. The ratios (last column) are more stable than absolute milliseconds.
|
||||
- Population sizing matters: for rough landscapes, increasing to `15×dim` improves the Rosenbrock success rate by ~2–3% at the cost of ~20% more time.
|
||||
|
||||
**Additional workloads (see notebook cells):**
|
||||
|
||||
- High-dimension stress test: Rastrigin 50D, population 800, 700 iterations (shows scaling trend)
|
||||
- HMM forward-backward throughput: synthetic 3-state Gaussian emissions (Rust vs pure Python)
|
||||
- MFG solver timing: 100×100 grid vs 150×150 grid (observed ~1.8× runtime increase, stable memory)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
- **0.3.0**
|
||||
- Added Mean Field Games Rust bindings
|
||||
- Expanded risk metrics utilities
|
||||
- Improved Python API consistency and documentation
|
||||
|
||||
- **0.2.x**
|
||||
- Initial public release with DE, HMM, MCMC, sparse optimization
|
||||
@@ -0,0 +1,86 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../../python'))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
project = 'Optimiz-rs'
|
||||
copyright = '2026, HFThot Research Lab'
|
||||
author = 'HFThot Research Lab'
|
||||
release = '0.3.0'
|
||||
version = '0.3.0'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.mathjax',
|
||||
'myst_parser',
|
||||
'sphinxcontrib.mermaid',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
html_theme = 'furo' # Modern, clean theme
|
||||
html_static_path = ['_static']
|
||||
html_title = 'Optimiz-rs Documentation'
|
||||
html_short_title = 'Optimiz-rs'
|
||||
html_logo = 'logo_optimizrs_transparent.png'
|
||||
html_favicon = 'logo_optimizrs_transparent.png'
|
||||
|
||||
html_theme_options = {
|
||||
"light_css_variables": {
|
||||
"color-brand-primary": "#f97316",
|
||||
"color-brand-content": "#f97316",
|
||||
},
|
||||
"dark_css_variables": {
|
||||
"color-brand-primary": "#fb923c",
|
||||
"color-brand-content": "#fb923c",
|
||||
},
|
||||
}
|
||||
|
||||
# Napoleon settings for Google/NumPy docstring parsing
|
||||
napoleon_google_docstring = True
|
||||
napoleon_numpy_docstring = True
|
||||
napoleon_include_init_with_doc = False
|
||||
napoleon_include_private_with_doc = False
|
||||
napoleon_include_special_with_doc = True
|
||||
napoleon_use_admonition_for_examples = False
|
||||
napoleon_use_admonition_for_notes = False
|
||||
napoleon_use_admonition_for_references = False
|
||||
napoleon_use_ivar = False
|
||||
napoleon_use_param = True
|
||||
napoleon_use_rtype = True
|
||||
napoleon_preprocess_types = False
|
||||
napoleon_type_aliases = None
|
||||
napoleon_attr_annotations = True
|
||||
|
||||
# Intersphinx configuration
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3', None),
|
||||
'numpy': ('https://numpy.org/doc/stable/', None),
|
||||
}
|
||||
|
||||
# MyST parser configuration for markdown support
|
||||
myst_enable_extensions = [
|
||||
"colon_fence",
|
||||
"deflist",
|
||||
"dollarmath",
|
||||
]
|
||||
|
||||
# Custom CSS
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
# Mermaid configuration
|
||||
mermaid_version = "10.9.0"
|
||||
mermaid_init_js = "mermaid.initialize({startOnLoad:true, theme:'dark', themeVariables:{primaryColor:'#f97316',primaryTextColor:'#fff',primaryBorderColor:'#ea6a0a',lineColor:'#fb923c',secondaryColor:'#1e293b',tertiaryColor:'#0f172a'}});"
|
||||
@@ -0,0 +1,7 @@
|
||||
# Contributing
|
||||
|
||||
- Open an issue describing the feature or bugfix.
|
||||
- Keep dependencies minimal; prefer NumPy/Matplotlib for examples.
|
||||
- Add or update tests under `tests/` when changing behavior.
|
||||
- Run `pytest` and `make html` in `docs/` before submitting a PR.
|
||||
- Follow Rust fmt and Python formatting conventions in the repo.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Examples
|
||||
|
||||
Practical snippets for every Optimiz-rs component.
|
||||
|
||||
## Differential Evolution (global optimization)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import differential_evolution
|
||||
|
||||
def sphere(x):
|
||||
return np.sum(x**2)
|
||||
|
||||
best_x, best_fx = differential_evolution(
|
||||
objective_fn=sphere,
|
||||
bounds=[(-10, 10)] * 5,
|
||||
strategy="rand1",
|
||||
maxiter=300,
|
||||
adaptive=True,
|
||||
)
|
||||
|
||||
print(best_fx)
|
||||
```
|
||||
|
||||
## Grid Search (hyper-parameter sweep)
|
||||
|
||||
```python
|
||||
from optimizr import grid_search
|
||||
|
||||
def objective(params):
|
||||
lr, momentum = params["lr"], params["momentum"]
|
||||
return (lr - 0.05)**2 + (momentum - 0.9)**2
|
||||
|
||||
best_params, best_score = grid_search(
|
||||
objective_fn=objective,
|
||||
param_grid={"lr": [0.01, 0.05, 0.1], "momentum": [0.8, 0.9, 0.95]},
|
||||
)
|
||||
|
||||
print(best_params, best_score)
|
||||
```
|
||||
|
||||
## Hidden Markov Models (regime detection)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import HMM
|
||||
|
||||
returns = np.random.randn(800) * 0.02 + 0.005
|
||||
returns[400:] -= 0.015 # regime shift
|
||||
|
||||
model = HMM(n_states=2).fit(returns)
|
||||
states = model.predict(returns)
|
||||
print(np.bincount(states))
|
||||
```
|
||||
|
||||
## MCMC (posterior sampling)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import mcmc_sample
|
||||
|
||||
def log_likelihood(params, data):
|
||||
mu, sigma = params
|
||||
residuals = (data - mu) / sigma
|
||||
return -0.5 * np.sum(residuals**2) - len(data) * np.log(sigma)
|
||||
|
||||
data = np.random.randn(500) + 1.0
|
||||
samples = mcmc_sample(
|
||||
log_likelihood_fn=log_likelihood,
|
||||
data=data,
|
||||
initial_params=np.array([0.0, 1.0]),
|
||||
param_bounds=[(-5, 5), (0.1, 5.0)],
|
||||
)
|
||||
print(samples.mean(axis=0))
|
||||
```
|
||||
|
||||
## Mean Field Games (1D solver)
|
||||
|
||||
```python
|
||||
from optimizr import MFGConfig, solve_mfg_1d_rust
|
||||
|
||||
config = MFGConfig(nx=64, nt=32, x_min=-2.0, x_max=2.0, T=1.0, epsilon=0.1, kappa=1.0)
|
||||
solution = solve_mfg_1d_rust(config)
|
||||
print(solution.converged)
|
||||
```
|
||||
|
||||
## Sparse Optimization (Sparse PCA)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import sparse_pca_py
|
||||
|
||||
X = np.random.randn(200, 10)
|
||||
components = sparse_pca_py(X, n_components=3, l1_ratio=0.2)
|
||||
print(components.shape)
|
||||
```
|
||||
|
||||
## Risk Metrics (time series)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from optimizr import hurst_exponent_py, estimate_half_life_py
|
||||
|
||||
returns = np.random.randn(1000) * 0.01
|
||||
print("Hurst:", hurst_exponent_py(returns))
|
||||
print("Half-life:", estimate_half_life_py(returns))
|
||||
```
|
||||
|
||||
## Notebooks
|
||||
|
||||
- Differential Evolution: `examples/notebooks/03_differential_evolution_tutorial.ipynb`
|
||||
- Mean Field Games: `examples/notebooks/mean_field_games_tutorial.ipynb`
|
||||
- HMM: `examples/notebooks/01_hmm_tutorial.ipynb`
|
||||
- MCMC: `examples/notebooks/02_mcmc_tutorial.ipynb`
|
||||
- Optimal Control & Kalman: `examples/notebooks/03_optimal_control_tutorial.ipynb`
|
||||
- Performance benchmarks: `examples/notebooks/05_performance_benchmarks.ipynb`
|
||||
|
||||
## Contribute Examples
|
||||
|
||||
1. Fork the repository and add notebooks under `examples/notebooks/`
|
||||
2. Keep dependencies minimal (NumPy/Matplotlib preferred)
|
||||
3. Ensure the notebook runs end-to-end before submitting a PR
|
||||
@@ -0,0 +1,53 @@
|
||||
# Getting Started
|
||||
|
||||
This guide prepares a fresh environment, builds the Rust extension, and validates the install.
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
```bash
|
||||
# Python deps for docs and benchmarks
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r docs/requirements.txt
|
||||
pip install maturin numpy
|
||||
```
|
||||
|
||||
## 2. Build and install Optimiz-rs locally
|
||||
|
||||
```bash
|
||||
pip install .
|
||||
# or editable mode for development
|
||||
maturin develop --release
|
||||
```
|
||||
|
||||
## 3. Quick verification
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
import optimizr
|
||||
from optimizr import differential_evolution, HMM
|
||||
print("Optimiz-rs version:", optimizr.__version__)
|
||||
|
||||
# Simple objective
|
||||
f = lambda x: sum(v * v for v in x)
|
||||
pt, val = differential_evolution(f, bounds=[(-2, 2)] * 3, maxiter=50)
|
||||
print("DE ok →", round(float(val), 4))
|
||||
|
||||
model = HMM(n_states=2).fit([0.01, -0.02, 0.0, 0.03])
|
||||
print("HMM states →", model.predict([0.01, -0.02, 0.0, 0.03]))
|
||||
PY
|
||||
```
|
||||
|
||||
## 4. Build docs locally
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
make html # or: sphinx-build -b html source build/html
|
||||
open build/html/index.html
|
||||
```
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
- If the Rust extension fails to compile, ensure `rustc --version` ≥ 1.70 and `maturin` is installed.
|
||||
- On Apple Silicon, set `export MACOSX_DEPLOYMENT_TARGET=12.0` before building if you hit ABI errors.
|
||||
- Delete stale builds with `rm -rf build dist target *.egg-info` then reinstall.
|
||||
@@ -0,0 +1,149 @@
|
||||
.. Optimiz-rs documentation master file
|
||||
|
||||
Optimiz-rs Documentation
|
||||
======================
|
||||
|
||||
**High-performance optimization algorithms in Rust with Python bindings**
|
||||
|
||||
.. image:: https://img.shields.io/badge/version-1.0.0-blue.svg
|
||||
:target: https://github.com/ThotDjehuty/optimiz-r/releases
|
||||
:alt: Version
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-MIT-green.svg
|
||||
:target: https://github.com/ThotDjehuty/optimiz-r/blob/main/LICENSE
|
||||
:alt: License
|
||||
|
||||
Optimiz-rs provides blazingly fast, production-ready implementations of advanced optimization and statistical inference algorithms. Built with Rust for maximum performance and exposed to Python through PyO3, it delivers **50-100× speedup** over pure Python implementations.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Getting Started
|
||||
|
||||
getting-started
|
||||
installation
|
||||
quickstart
|
||||
examples
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Algorithms
|
||||
|
||||
algorithms/differential_evolution
|
||||
algorithms/mean_field_games
|
||||
algorithms/hmm
|
||||
algorithms/mcmc
|
||||
algorithms/sparse_optimization
|
||||
algorithms/optimal_control
|
||||
algorithms/risk_metrics
|
||||
algorithms/grid_search
|
||||
algorithms/point_processes
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: v1.1 Numerical Primitives
|
||||
|
||||
algorithms/matrix_riccati
|
||||
algorithms/nonsync_covariance
|
||||
algorithms/wavelet
|
||||
algorithms/risk_measures
|
||||
algorithms/graph_spectral
|
||||
algorithms/topology
|
||||
algorithms/volterra
|
||||
algorithms/signatures
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: API Reference
|
||||
|
||||
api/differential_evolution
|
||||
api/grid_search
|
||||
api/hmm
|
||||
api/mcmc
|
||||
api/sparse
|
||||
api/optimal_control
|
||||
api/risk_metrics
|
||||
api/point_processes
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Advanced
|
||||
|
||||
theory/mathematical_foundations
|
||||
mfg_tutorial
|
||||
benchmarks
|
||||
contributing
|
||||
changelog
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
✨ **Algorithms Included:**
|
||||
|
||||
- **Mean Field Games**: 1D MFG solver, HJB-Fokker-Planck coupling, agent population dynamics
|
||||
- **Differential Evolution**: 5 strategies (rand/1, best/1, current-to-best/1, rand/2, best/2), adaptive jDE
|
||||
- **Optimal Control**: HJB solvers, regime switching, jump diffusion, MRSJD framework
|
||||
- **Hidden Markov Models**: Baum-Welch training, Viterbi decoding, Gaussian emissions
|
||||
- **MCMC Sampling**: Metropolis-Hastings, adaptive proposals, Bayesian inference
|
||||
- **Sparse Optimization**: Sparse PCA, Box-Tao decomposition, Elastic Net, ADMM
|
||||
- **Risk Metrics**: Hurst exponent, half-life estimation, time series analysis
|
||||
- **Information Theory**: Mutual information, Shannon entropy, feature selection
|
||||
- **Time-Series Helpers**: Rolling Hurst/half-life, feature prep for HMM, lagged feature builders
|
||||
- **Parallelization**: Rust-native population evaluation with Rayon for built-in objectives
|
||||
|
||||
🚀 **Performance:**
|
||||
|
||||
- **50-100× faster** than pure Python implementations
|
||||
- **95% memory reduction** vs NumPy/SciPy
|
||||
- **Parallel-ready** with Rayon infrastructure
|
||||
- Production-tested on multi-dimensional problems
|
||||
|
||||
Quick Example
|
||||
-------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import numpy as np
|
||||
from optimizr import DifferentialEvolution
|
||||
|
||||
# Define objective function
|
||||
def sphere(x):
|
||||
return np.sum(x**2)
|
||||
|
||||
# Optimize
|
||||
de = DifferentialEvolution(
|
||||
bounds=[(-5, 5)] * 10,
|
||||
strategy="best/1/bin",
|
||||
population_size=50
|
||||
)
|
||||
result = de.optimize(sphere, max_iterations=100)
|
||||
|
||||
print(f"Best fitness: {result.best_fitness:.6f}")
|
||||
print(f"Best solution: {result.best_solution}")
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
From PyPI (coming soon):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install optimizr
|
||||
|
||||
From source:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Clone repository
|
||||
git clone https://github.com/ThotDjehuty/optimiz-r.git
|
||||
cd optimiz-r
|
||||
|
||||
# Build and install
|
||||
pip install maturin
|
||||
maturin develop --release
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
@@ -0,0 +1,118 @@
|
||||
# Installation Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8 or higher
|
||||
- Rust 1.70 or higher (for building from source)
|
||||
- pip
|
||||
|
||||
## Install from PyPI
|
||||
|
||||
**Coming soon**: Optimiz-rs will be available on PyPI.
|
||||
|
||||
```bash
|
||||
pip install optimizr
|
||||
```
|
||||
|
||||
## Install from Source
|
||||
|
||||
### Step 1: Clone Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ThotDjehuty/optimiz-r.git
|
||||
cd optimiz-r
|
||||
```
|
||||
|
||||
### Step 2: Install Maturin
|
||||
|
||||
[Maturin](https://github.com/PyO3/maturin) is required to build Rust-Python bindings:
|
||||
|
||||
```bash
|
||||
pip install maturin
|
||||
```
|
||||
|
||||
### Step 3: Build and Install
|
||||
|
||||
**Development mode** (editable install, useful for development):
|
||||
|
||||
```bash
|
||||
maturin develop --release
|
||||
```
|
||||
|
||||
**Production install** (creates wheel and installs):
|
||||
|
||||
```bash
|
||||
maturin build --release
|
||||
pip install target/wheels/optimizr-*.whl
|
||||
```
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```python
|
||||
import optimizr
|
||||
print(optimizr.__version__) # Should print "0.3.0"
|
||||
```
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### macOS
|
||||
|
||||
If you encounter build errors on macOS:
|
||||
|
||||
1. Ensure Xcode Command Line Tools are installed:
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
2. Install Rust via rustup:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
1. Install Visual Studio Build Tools (2019 or later)
|
||||
2. Install Rust via [rustup-init.exe](https://rustup.rs/)
|
||||
3. Follow standard installation steps
|
||||
|
||||
### Linux
|
||||
|
||||
Requires GCC or Clang:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install build-essential
|
||||
|
||||
# If you see OpenBLAS link errors during wheels/docs build
|
||||
sudo apt-get install libopenblas-dev
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install gcc gcc-c++
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: `maturin: command not found`
|
||||
|
||||
**Solution**: Ensure pip bin directory is in PATH:
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH" # Linux/macOS
|
||||
```
|
||||
|
||||
**Issue**: Rust compiler errors
|
||||
|
||||
**Solution**: Update Rust to latest stable:
|
||||
```bash
|
||||
rustup update stable
|
||||
```
|
||||
|
||||
**Issue**: ImportError when importing optimizr
|
||||
|
||||
**Solution**: Rebuild with correct Python version:
|
||||
```bash
|
||||
maturin develop --release -i python3.10 # Replace with your Python version
|
||||
```
|
||||
|
||||
**Issue**: BLAS/LAPACK linkage errors on Linux
|
||||
|
||||
**Solution**: Install OpenBLAS headers (see Linux section above) and rebuild with `maturin develop --release`.
|
||||