- Fix format_push_string: write!() instead of push_str(&format!()) (25 sites) - Fix str_to_string: .to_owned() instead of .to_string() on &str (6 sites) - Fix unseparated_literal_suffix: add _ separator (6 sites) - Fix multiple_inherent_impl: merge split impl blocks in TGGN, TFT, OFI (3) - Fix else_if_without_else: add exhaustive else clauses (3 sites) - Fix if_then_some_else_none: use .then().transpose() (1 site) - Fix unwrap_in_result: replace expect() with match + ? (2 sites) - Fix wildcard_enum_match_arm: enumerate Storage variants explicitly (2) - Fix decimal_literal_representation: use hex for power-of-2 constants (5) - Fix rc_buffer: Arc<Vec<T>> → Arc<[T]> for OFI features - Fix needless_range_loop: convert to iterator patterns (17 sites) - Fix used_underscore_binding: remove prefix on used vars (6 sites) - Fix doc list item indentation (7 sites) - Allow too_many_arguments on ML training functions (4) - Allow multiple_unsafe_ops_per_block on CUDA FFI functions (3) - Allow upper_case_acronyms on SLSTM/MLSTM model names (2) - Add ML-crate pedantic allows: shadow, similar_names, type_complexity, indexing_slicing, partial_pub_fields, non_ascii_literal, same_name_method (following existing ml-labeling/ml-universe pattern) Result: cargo clippy --workspace -- -D warnings passes with zero warnings. All 2758+ lib tests pass (2 pre-existing backtesting failures unchanged). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
412 lines
12 KiB
Rust
412 lines
12 KiB
Rust
// ml/src/features/barrier_optimization.rs
|
|
//
|
|
// Barrier Optimization Engine
|
|
// Optimizes triple barrier parameters via grid search + Sharpe maximization
|
|
|
|
use std::fmt;
|
|
use std::time::Instant;
|
|
|
|
/// Parameters for triple barrier labeling
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct BarrierParams {
|
|
pub profit_factor: f64,
|
|
pub stop_factor: f64,
|
|
pub time_horizon: usize,
|
|
}
|
|
|
|
impl BarrierParams {
|
|
/// Create new barrier parameters with validation
|
|
pub fn new(profit_factor: f64, stop_factor: f64, time_horizon: usize) -> Self {
|
|
assert!(
|
|
profit_factor > 0.0,
|
|
"profit_factor must be positive, got {}",
|
|
profit_factor
|
|
);
|
|
assert!(
|
|
stop_factor > 0.0,
|
|
"stop_factor must be positive, got {}",
|
|
stop_factor
|
|
);
|
|
assert!(
|
|
time_horizon >= 1,
|
|
"time_horizon must be at least 1, got {}",
|
|
time_horizon
|
|
);
|
|
|
|
Self {
|
|
profit_factor,
|
|
stop_factor,
|
|
time_horizon,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for BarrierParams {
|
|
fn default() -> Self {
|
|
Self {
|
|
profit_factor: 2.0,
|
|
stop_factor: 1.0,
|
|
time_horizon: 10,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of barrier optimization
|
|
#[derive(Debug, Clone)]
|
|
pub struct OptimizationResult {
|
|
pub best_params: BarrierParams,
|
|
pub best_sharpe: f64,
|
|
pub evaluations: usize,
|
|
pub duration_ms: u128,
|
|
}
|
|
|
|
impl fmt::Display for OptimizationResult {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"OptimizationResult {{ \
|
|
profit_factor: {:.2}, \
|
|
stop_factor: {:.2}, \
|
|
time_horizon: {}, \
|
|
sharpe: {:.4}, \
|
|
evaluations: {}, \
|
|
duration_ms: {} }}",
|
|
self.best_params.profit_factor,
|
|
self.best_params.stop_factor,
|
|
self.best_params.time_horizon,
|
|
self.best_sharpe,
|
|
self.evaluations,
|
|
self.duration_ms
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Barrier parameter optimizer using grid search
|
|
#[derive(Debug)]
|
|
pub struct BarrierOptimizer {
|
|
profit_range: Vec<f64>,
|
|
stop_range: Vec<f64>,
|
|
horizon_range: Vec<usize>,
|
|
}
|
|
|
|
impl BarrierOptimizer {
|
|
/// Create optimizer with default search ranges
|
|
pub fn new() -> Self {
|
|
Self {
|
|
profit_range: vec![1.0, 1.5, 2.0, 2.5, 3.0],
|
|
stop_range: vec![0.5, 1.0, 1.5, 2.0],
|
|
horizon_range: vec![5, 10, 20, 30],
|
|
}
|
|
}
|
|
|
|
/// Create optimizer with custom search ranges
|
|
pub const fn with_ranges(
|
|
profit_range: Vec<f64>,
|
|
stop_range: Vec<f64>,
|
|
horizon_range: Vec<usize>,
|
|
) -> Self {
|
|
Self {
|
|
profit_range,
|
|
stop_range,
|
|
horizon_range,
|
|
}
|
|
}
|
|
|
|
/// Get profit factor search range
|
|
pub fn profit_range(&self) -> &[f64] {
|
|
&self.profit_range
|
|
}
|
|
|
|
/// Get stop factor search range
|
|
pub fn stop_range(&self) -> &[f64] {
|
|
&self.stop_range
|
|
}
|
|
|
|
/// Get time horizon search range
|
|
pub fn horizon_range(&self) -> &[usize] {
|
|
&self.horizon_range
|
|
}
|
|
|
|
/// Get total number of parameter combinations
|
|
pub fn total_combinations(&self) -> usize {
|
|
self.profit_range.len() * self.stop_range.len() * self.horizon_range.len()
|
|
}
|
|
|
|
/// Optimize barrier parameters using grid search
|
|
///
|
|
/// # Arguments
|
|
/// * `prices` - Historical price data for backtesting
|
|
///
|
|
/// # Returns
|
|
/// Optimal parameters and Sharpe ratio
|
|
pub fn optimize(&self, prices: &[f64]) -> OptimizationResult {
|
|
let start = Instant::now();
|
|
|
|
let mut best_sharpe = f64::NEG_INFINITY;
|
|
let mut best_params = BarrierParams::default();
|
|
let mut evaluations = 0;
|
|
|
|
// Grid search over all parameter combinations
|
|
for &profit in &self.profit_range {
|
|
for &stop in &self.stop_range {
|
|
for &horizon in &self.horizon_range {
|
|
let params = BarrierParams::new(profit, stop, horizon);
|
|
let sharpe = self.backtest_params(¶ms, prices);
|
|
|
|
evaluations += 1;
|
|
|
|
if sharpe > best_sharpe && sharpe.is_finite() {
|
|
best_sharpe = sharpe;
|
|
best_params = params;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no valid Sharpe found, use 0.0
|
|
if !best_sharpe.is_finite() {
|
|
best_sharpe = 0.0;
|
|
}
|
|
|
|
let duration = start.elapsed();
|
|
|
|
OptimizationResult {
|
|
best_params,
|
|
best_sharpe,
|
|
evaluations,
|
|
duration_ms: duration.as_millis(),
|
|
}
|
|
}
|
|
|
|
/// Backtest specific barrier parameters and return Sharpe ratio
|
|
///
|
|
/// # Arguments
|
|
/// * `params` - Barrier parameters to test
|
|
/// * `prices` - Historical price data
|
|
///
|
|
/// # Returns
|
|
/// Sharpe ratio for the given parameters
|
|
pub fn backtest_params(&self, params: &BarrierParams, prices: &[f64]) -> f64 {
|
|
// Handle edge cases
|
|
if prices.is_empty() || prices.len() < 2 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Filter out NaN and infinite values
|
|
// Preallocate: worst case is all prices are clean
|
|
let mut clean_prices = Vec::with_capacity(prices.len());
|
|
clean_prices.extend(prices.iter().copied().filter(|&p| p.is_finite()));
|
|
|
|
if clean_prices.len() < 2 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Generate trading signals using triple barrier logic
|
|
let returns = self.simulate_triple_barrier_trading(params, &clean_prices);
|
|
|
|
// Calculate Sharpe ratio
|
|
self.calculate_sharpe(&returns)
|
|
}
|
|
|
|
/// Simulate triple barrier trading strategy
|
|
///
|
|
/// For each entry point, we:
|
|
/// 1. Calculate profit target: entry * (1 + `profit_factor` * volatility)
|
|
/// 2. Calculate stop loss: entry * (1 - `stop_factor` * volatility)
|
|
/// 3. Hold for up to `time_horizon` periods
|
|
/// 4. Exit when price hits barrier or horizon reached
|
|
fn simulate_triple_barrier_trading(&self, params: &BarrierParams, prices: &[f64]) -> Vec<f64> {
|
|
// Preallocate capacity: estimate max trades as prices.len() / time_horizon
|
|
// This prevents unbounded growth and reduces allocations
|
|
let estimated_trades = prices.len().saturating_div(params.time_horizon.max(1));
|
|
let mut returns = Vec::with_capacity(estimated_trades);
|
|
|
|
// Need at least time_horizon + 1 prices for meaningful backtest
|
|
if prices.len() < params.time_horizon + 1 {
|
|
return returns;
|
|
}
|
|
|
|
// Calculate rolling volatility (using simple std dev over 20 periods)
|
|
let vol_window = 20.min(prices.len() / 2);
|
|
|
|
// Iterate through potential entry points
|
|
let mut i = vol_window;
|
|
while i < prices.len() - params.time_horizon {
|
|
let entry_price = prices[i];
|
|
|
|
// Calculate volatility from recent price changes
|
|
let volatility = self.calculate_volatility(&prices[i.saturating_sub(vol_window)..=i]);
|
|
|
|
if volatility <= 0.0 || !volatility.is_finite() {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
// Set barriers
|
|
let profit_target = entry_price * (1.0 + params.profit_factor * volatility);
|
|
let stop_loss = entry_price * (1.0 - params.stop_factor * volatility);
|
|
|
|
// Simulate holding period
|
|
let mut exit_price = entry_price;
|
|
let max_horizon = (i + params.time_horizon).min(prices.len() - 1);
|
|
|
|
for ¤t_price in &prices[(i + 1)..=max_horizon] {
|
|
|
|
// Check if profit target hit
|
|
if current_price >= profit_target {
|
|
exit_price = profit_target;
|
|
break;
|
|
}
|
|
|
|
// Check if stop loss hit
|
|
if current_price <= stop_loss {
|
|
exit_price = stop_loss;
|
|
break;
|
|
}
|
|
|
|
// Update exit price (will use this if horizon reached)
|
|
exit_price = current_price;
|
|
}
|
|
|
|
// Calculate return
|
|
let trade_return = (exit_price - entry_price) / entry_price;
|
|
returns.push(trade_return);
|
|
|
|
// Move to next entry point (skip ahead to avoid overlapping trades)
|
|
i += params.time_horizon;
|
|
}
|
|
|
|
returns
|
|
}
|
|
|
|
/// Calculate volatility as standard deviation of returns
|
|
fn calculate_volatility(&self, prices: &[f64]) -> f64 {
|
|
if prices.len() < 2 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Calculate returns
|
|
// Preallocate: max returns is prices.len() - 1 (one per window)
|
|
let mut returns = Vec::with_capacity(prices.len().saturating_sub(1));
|
|
returns.extend(
|
|
prices
|
|
.windows(2)
|
|
.map(|w| (w[1] - w[0]) / w[0])
|
|
.filter(|r| r.is_finite()),
|
|
);
|
|
|
|
if returns.is_empty() {
|
|
return 0.0;
|
|
}
|
|
|
|
// Calculate standard deviation
|
|
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
|
|
let variance =
|
|
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
|
|
variance.sqrt()
|
|
}
|
|
|
|
/// Calculate Sharpe ratio from returns
|
|
///
|
|
/// Sharpe = (`mean_return` - `risk_free_rate`) / `std_dev_return`
|
|
/// Assuming `risk_free_rate` = 0 for simplicity
|
|
pub fn calculate_sharpe(&self, returns: &[f64]) -> f64 {
|
|
if returns.is_empty() {
|
|
return 0.0;
|
|
}
|
|
|
|
// Filter out non-finite values
|
|
// Preallocate: worst case is all returns are clean
|
|
let mut clean_returns = Vec::with_capacity(returns.len());
|
|
clean_returns.extend(returns.iter().copied().filter(|r| r.is_finite()));
|
|
|
|
if clean_returns.is_empty() {
|
|
return 0.0;
|
|
}
|
|
|
|
// Calculate mean return
|
|
let mean_return = clean_returns.iter().sum::<f64>() / clean_returns.len() as f64;
|
|
|
|
// Calculate standard deviation of returns
|
|
let variance = clean_returns
|
|
.iter()
|
|
.map(|r| (r - mean_return).powi(2))
|
|
.sum::<f64>()
|
|
/ clean_returns.len() as f64;
|
|
|
|
let std_dev = variance.sqrt();
|
|
|
|
// Handle zero volatility case
|
|
if std_dev < 1e-10 {
|
|
// If std_dev is near zero and mean is positive, return large Sharpe
|
|
// If std_dev is near zero and mean is zero/negative, return 0
|
|
if mean_return > 1e-10 {
|
|
return 100.0; // Cap at reasonable value
|
|
} else {
|
|
return 0.0;
|
|
}
|
|
}
|
|
|
|
// Calculate Sharpe ratio (annualized: multiply by sqrt(252) for daily data)
|
|
// For now, return non-annualized Sharpe
|
|
mean_return / std_dev
|
|
}
|
|
}
|
|
|
|
impl Default for BarrierOptimizer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_barrier_params_creation() {
|
|
let params = BarrierParams::new(2.0, 1.0, 10);
|
|
assert_eq!(params.profit_factor, 2.0);
|
|
assert_eq!(params.stop_factor, 1.0);
|
|
assert_eq!(params.time_horizon, 10);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_barrier_params_invalid_profit() {
|
|
BarrierParams::new(-1.0, 1.0, 10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_optimizer_creation() {
|
|
let optimizer = BarrierOptimizer::new();
|
|
assert_eq!(optimizer.total_combinations(), 80); // 5 * 4 * 4
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_sharpe_basic() {
|
|
let optimizer = BarrierOptimizer::new();
|
|
let returns = vec![0.01, 0.02, -0.005, 0.015, 0.008];
|
|
let sharpe = optimizer.calculate_sharpe(&returns);
|
|
assert!(sharpe.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_volatility() {
|
|
let optimizer = BarrierOptimizer::new();
|
|
let prices = vec![100.0, 101.0, 99.0, 102.0, 98.0];
|
|
let vol = optimizer.calculate_volatility(&prices);
|
|
assert!(vol > 0.0);
|
|
assert!(vol.is_finite());
|
|
}
|
|
|
|
#[test]
|
|
fn test_optimize_simple() {
|
|
let optimizer = BarrierOptimizer::new();
|
|
let prices: Vec<f64> = (0..50).map(|i| 100.0 + i as f64).collect();
|
|
let result = optimizer.optimize(&prices);
|
|
assert!(result.best_sharpe.is_finite());
|
|
assert_eq!(result.evaluations, 80);
|
|
}
|
|
}
|