Files
optimiz-rs/src/timeseries_utils.rs
T

434 lines
12 KiB
Rust

//! Time-Series Utilities for Finance and Trading
//!
//! Helper functions for common workflows combining time-series preprocessing
//! with optimization and statistical inference. Designed to work seamlessly
//! with Polarway time-series operations and OptimizR algorithms.
//!
//! # Use Cases
//!
//! - Regime detection with HMM
//! - Rolling risk metrics (Hurst exponent, half-life)
//! - Trading strategy parameter optimization
//! - Feature engineering for financial data
//!
//! # Examples
//!
//! ```rust
//! use optimizr::timeseries_utils::*;
//!
//! // Prepare features for HMM regime detection
//! let prices = vec![100.0, 101.0, 99.5, 102.0];
//! let features = prepare_for_hmm(&prices, &[1, 5]);
//!
//! // Rolling Hurst exponent
//! let returns = vec![0.01, -0.02, 0.015, 0.005];
//! let rolling_hurst = rolling_hurst_exponent(&returns, 20);
//! ```
use crate::risk_metrics::{hurst_exponent, estimate_half_life};
use ndarray::Array1;
#[cfg(feature = "python-bindings")]
pub mod python_bindings;
/// Prepare time-series price data for HMM regime detection
///
/// Creates features from price series including:
/// - Returns (percent change)
/// - Lagged returns
/// - Log returns
/// - Volatility proxy (absolute returns)
///
/// # Arguments
///
/// * `prices` - Raw price series (e.g., stock prices, crypto prices)
/// * `lag_periods` - Lags to compute for returns (e.g., [1, 5, 20] for daily, weekly, monthly)
///
/// # Returns
///
/// Matrix where each row is a feature vector suitable for HMM training.
/// First row will have NaN values due to lagging.
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::prepare_for_hmm;
///
/// let prices = vec![100.0, 101.0, 99.5, 102.0, 103.5];
/// let features = prepare_for_hmm(&prices, &[1, 2]);
///
/// // Features: [return_t, return_t-1, return_t-2, log_return, abs_return]
/// assert_eq!(features[0].len(), 5);
/// ```
pub fn prepare_for_hmm(prices: &[f64], lag_periods: &[usize]) -> Vec<Vec<f64>> {
let n = prices.len();
if n < 2 {
return vec![];
}
// Calculate returns
let mut returns = Vec::with_capacity(n - 1);
let mut log_returns = Vec::with_capacity(n - 1);
let mut abs_returns = Vec::with_capacity(n - 1);
for i in 1..n {
let ret = (prices[i] - prices[i - 1]) / prices[i - 1];
returns.push(ret);
log_returns.push((prices[i] / prices[i - 1]).ln());
abs_returns.push(ret.abs());
}
// Create feature matrix
let max_lag = *lag_periods.iter().max().unwrap_or(&0);
let mut features = Vec::new();
for t in max_lag..returns.len() {
let mut feature_vec = Vec::new();
// Current return
feature_vec.push(returns[t]);
// Lagged returns
for &lag in lag_periods {
if t >= lag {
feature_vec.push(returns[t - lag]);
} else {
feature_vec.push(f64::NAN);
}
}
// Log return and volatility proxy
feature_vec.push(log_returns[t]);
feature_vec.push(abs_returns[t]);
features.push(feature_vec);
}
features
}
/// Compute Hurst exponent in rolling windows
///
/// The Hurst exponent H indicates:
/// - H < 0.5: Mean-reverting series
/// - H = 0.5: Random walk (no memory)
/// - H > 0.5: Trending series (momentum)
///
/// # Arguments
///
/// * `returns` - Return series (percent changes or log returns)
/// * `window_size` - Rolling window size (e.g., 252 for 1 year of daily data)
///
/// # Returns
///
/// Vector of Hurst exponents, one per window. Length = returns.len() - window_size + 1
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::rolling_hurst_exponent;
///
/// let returns = vec![0.01; 300]; // Synthetic data
/// let rolling_h = rolling_hurst_exponent(&returns, 252);
///
/// assert_eq!(rolling_h.len(), 300 - 252 + 1);
/// ```
pub fn rolling_hurst_exponent(returns: &[f64], window_size: usize) -> Vec<f64> {
let n = returns.len();
if n < window_size {
return vec![];
}
let mut rolling_h = Vec::with_capacity(n - window_size + 1);
for i in 0..=(n - window_size) {
let window = &returns[i..i + window_size];
let window_arr = Array1::from_vec(window.to_vec());
// Use standard window sizes for R/S analysis
let window_sizes = vec![8, 16, 32].into_iter().filter(|&w| w <= window_size / 4).collect::<Vec<_>>();
let h = if window_sizes.is_empty() {
0.5 // Default to random walk if window too small
} else {
hurst_exponent(&window_arr, &window_sizes)
.map(|result| result.hurst_exponent)
.unwrap_or(0.5)
};
rolling_h.push(h);
}
rolling_h
}
/// Compute half-life of mean reversion in rolling windows
///
/// Half-life is the expected time for a price series to revert halfway
/// to its mean. Useful for pairs trading and mean-reversion strategies.
///
/// # Arguments
///
/// * `prices` - Price series (not returns)
/// * `window_size` - Rolling window size
///
/// # Returns
///
/// Vector of half-life estimates in same time units as data
/// (e.g., days if daily prices)
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::rolling_half_life;
///
/// let prices = vec![100.0; 300]; // Synthetic data
/// let rolling_hl = rolling_half_life(&prices, 100);
///
/// assert_eq!(rolling_hl.len(), 300 - 100 + 1);
/// ```
pub fn rolling_half_life(prices: &[f64], window_size: usize) -> Vec<f64> {
let n = prices.len();
if n < window_size {
return vec![];
}
let mut rolling_hl = Vec::with_capacity(n - window_size + 1);
for i in 0..=(n - window_size) {
let window = &prices[i..i + window_size];
let window_arr = Array1::from_vec(window.to_vec());
let hl = estimate_half_life(&window_arr).unwrap_or(f64::INFINITY);
rolling_hl.push(hl);
}
rolling_hl
}
/// Calculate basic time-series statistics
///
/// Returns summary statistics useful for risk analysis and diagnostics.
///
/// # Arguments
///
/// * `returns` - Return series
///
/// # Returns
///
/// Tuple of (mean, std_dev, skewness, kurtosis, sharpe_ratio)
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::return_statistics;
///
/// let returns = vec![0.01, -0.02, 0.015, 0.005, -0.01];
/// let (mean, std, skew, kurt, sharpe) = return_statistics(&returns);
/// ```
pub fn return_statistics(returns: &[f64]) -> (f64, f64, f64, f64, f64) {
let n = returns.len() as f64;
if returns.is_empty() {
return (0.0, 0.0, 0.0, 0.0, 0.0);
}
// Mean
let mean = returns.iter().sum::<f64>() / n;
// Standard deviation
let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
// Skewness
let skewness = if std_dev > 1e-10 {
returns.iter().map(|r| ((r - mean) / std_dev).powi(3)).sum::<f64>() / n
} else {
0.0
};
// Excess kurtosis
let kurtosis = if std_dev > 1e-10 {
returns.iter().map(|r| ((r - mean) / std_dev).powi(4)).sum::<f64>() / n - 3.0
} else {
0.0
};
// Sharpe ratio (assuming 252 trading days, risk-free rate = 0)
let sharpe_ratio = if std_dev > 1e-10 {
(mean * 252.0_f64.sqrt()) / std_dev
} else {
0.0
};
(mean, std_dev, skewness, kurtosis, sharpe_ratio)
}
/// Create lagged features for machine learning
///
/// Useful for creating supervised learning datasets from time series.
///
/// # Arguments
///
/// * `series` - Input time series (prices, returns, etc.)
/// * `lags` - Vector of lag periods (e.g., [1, 2, 5, 10])
/// * `include_original` - Whether to include t=0 (current value)
///
/// # Returns
///
/// Matrix where each row is [t, t-lag1, t-lag2, ...] if include_original=true
/// or [t-lag1, t-lag2, ...] if false
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::create_lagged_features;
///
/// let prices = vec![100.0, 101.0, 99.5, 102.0, 103.5, 104.0];
/// let features = create_lagged_features(&prices, &[1, 2], true);
///
/// // Each row: [price_t, price_t-1, price_t-2]
/// ```
pub fn create_lagged_features(series: &[f64], lags: &[usize], include_original: bool) -> Vec<Vec<f64>> {
let n = series.len();
let max_lag = *lags.iter().max().unwrap_or(&0);
if n <= max_lag {
return vec![];
}
let mut features = Vec::new();
for t in max_lag..n {
let mut feature_vec = Vec::new();
if include_original {
feature_vec.push(series[t]);
}
for &lag in lags {
feature_vec.push(series[t - lag]);
}
features.push(feature_vec);
}
features
}
/// Compute rolling correlation between two series
///
/// Useful for pairs trading and correlation analysis.
///
/// # Arguments
///
/// * `series1` - First time series
/// * `series2` - Second time series (must be same length as series1)
/// * `window_size` - Rolling window size
///
/// # Returns
///
/// Vector of correlation coefficients
///
/// # Example
///
/// ```
/// use optimizr::timeseries_utils::rolling_correlation;
///
/// let spy = vec![100.0, 101.0, 99.5, 102.0, 103.5];
/// let qqq = vec![200.0, 202.0, 199.0, 204.0, 207.0];
/// let corr = rolling_correlation(&spy, &qqq, 3);
/// ```
pub fn rolling_correlation(series1: &[f64], series2: &[f64], window_size: usize) -> Vec<f64> {
let n = series1.len();
if n != series2.len() || n < window_size {
return vec![];
}
let mut rolling_corr = Vec::with_capacity(n - window_size + 1);
for i in 0..=(n - window_size) {
let x = &series1[i..i + window_size];
let y = &series2[i..i + window_size];
// Compute correlation
let mean_x = x.iter().sum::<f64>() / window_size as f64;
let mean_y = y.iter().sum::<f64>() / window_size as f64;
let mut cov = 0.0;
let mut var_x = 0.0;
let mut var_y = 0.0;
for j in 0..window_size {
let dx = x[j] - mean_x;
let dy = y[j] - mean_y;
cov += dx * dy;
var_x += dx * dx;
var_y += dy * dy;
}
let corr = if var_x > 1e-10 && var_y > 1e-10 {
cov / (var_x.sqrt() * var_y.sqrt())
} else {
0.0
};
rolling_corr.push(corr);
}
rolling_corr
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prepare_for_hmm() {
let prices = vec![100.0, 101.0, 99.5, 102.0, 103.5, 104.0];
let features = prepare_for_hmm(&prices, &[1, 2]);
assert!(!features.is_empty());
// Each feature vector: [return, lag1, lag2, log_return, abs_return]
assert_eq!(features[0].len(), 5);
}
#[test]
fn test_rolling_hurst_exponent() {
let returns = vec![0.01; 50];
let rolling_h = rolling_hurst_exponent(&returns, 20);
assert_eq!(rolling_h.len(), 50 - 20 + 1);
// Constant series should have H close to 0.5
for h in rolling_h {
assert!((h - 0.5).abs() < 0.3); // Allow some numerical variation
}
}
#[test]
fn test_return_statistics() {
let returns = vec![0.01, -0.02, 0.015, 0.005, -0.01];
let (mean, std, _skew, _kurt, _sharpe) = return_statistics(&returns);
assert!((mean).abs() < 0.1); // Small mean
assert!(std > 0.0); // Non-zero volatility
// Skewness and kurtosis can vary widely
}
#[test]
fn test_create_lagged_features() {
let series = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let features = create_lagged_features(&series, &[1, 2], true);
assert_eq!(features.len(), 3); // 5 - 2 = 3 valid rows
assert_eq!(features[0], vec![3.0, 2.0, 1.0]); // t=2: [val_t, val_t-1, val_t-2]
}
#[test]
fn test_rolling_correlation() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![2.0, 4.0, 6.0, 8.0, 10.0]; // Perfect correlation
let corr = rolling_correlation(&x, &y, 3);
assert_eq!(corr.len(), 3);
for c in corr {
assert!((c - 1.0).abs() < 1e-6); // Perfect positive correlation
}
}
}