Extract 9 new sub-crates from the ml monolith to enable parallel compilation across the workspace: New crates (this commit): - ml-features (282 tests): feature engineering, 21 modules - ml-labeling (45 tests): triple barrier, meta-labeling, fractional diff - ml-ensemble (116 tests): ensemble coordination, voting, confidence - ml-hyperopt (47 tests): core PSO/TPE optimizer, parameter space - ml-checkpoint (41 tests): checkpoint persistence, compression, signing - ml-regime (68 tests): CUSUM, Bayesian changepoint, regime classification - ml-data-validation (67 tests): FDR correction, CPCV, data quality - ml-risk (33 tests): neural VaR, Kelly criterion, circuit breakers - ml-validation (43 tests): statistical validation, walk-forward, DSR Extended existing crates: - ml-dqn: added evaluation/ (backtesting engine, metrics, reports) and checkpoint implementation - ml-supervised: added checkpoint implementations - ml-core: added shared types needed by new sub-crates Pattern: each module in ml/ becomes a thin facade (pub use subcrate::*) with bridge modules staying in ml for cross-model adapter code. Dead code deleted (~7K lines): - 13 undeclared files in microstructure/ (never compiled) - 7 undeclared files + tests/ in risk/ (never compiled) - parquet_io, cache_service, cache_storage, minio_integration (unused) - extraction_wave_d_impl.rs (bare fn outside impl block) All 2,746 sub-crate tests + 951 ml tests pass. Full workspace builds clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
473 lines
16 KiB
Rust
473 lines
16 KiB
Rust
//! ML-Enhanced Circuit Breakers for HFT Risk Management
|
|
//!
|
|
//! Implements intelligent circuit breakers that use machine learning models
|
|
//! to detect market stress, model degradation, and risk anomalies with
|
|
//! canonical types for production HFT systems.
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use common::types::{Price, Volume};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{MLError, MLResult as Result};
|
|
|
|
/// Circuit breaker type enumeration
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
pub enum CircuitBreakerType {
|
|
/// Volatility-based circuit breaker
|
|
Volatility,
|
|
/// Drawdown-based circuit breaker
|
|
Drawdown,
|
|
/// Volume-based circuit breaker
|
|
Volume,
|
|
/// Model performance circuit breaker
|
|
ModelPerformance,
|
|
/// Market stress circuit breaker
|
|
MarketStress,
|
|
/// Position concentration circuit breaker
|
|
PositionConcentration,
|
|
}
|
|
|
|
/// Circuit breaker configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CircuitBreakerConfig {
|
|
pub volatility_threshold: f64,
|
|
pub drawdown_threshold: f64,
|
|
pub volume_spike_threshold: f64,
|
|
pub model_accuracy_threshold: f64,
|
|
pub max_position_size: f64,
|
|
pub lookback_period: usize,
|
|
pub cooldown_seconds: u64,
|
|
pub enable_auto_reset: bool,
|
|
}
|
|
|
|
impl Default for CircuitBreakerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
volatility_threshold: 0.05, // 5% volatility threshold
|
|
drawdown_threshold: 0.03, // 3% drawdown threshold
|
|
volume_spike_threshold: 3.0, // 3x normal volume
|
|
model_accuracy_threshold: 0.8, // 80% minimum accuracy
|
|
max_position_size: 0.1, // 10% max position size
|
|
lookback_period: 100, // 100 periods lookback
|
|
cooldown_seconds: 300, // 5 minute cooldown
|
|
enable_auto_reset: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Circuit breaker state
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CircuitBreakerState {
|
|
pub breaker_type: CircuitBreakerType,
|
|
pub is_triggered: bool,
|
|
pub trigger_time: Option<DateTime<Utc>>,
|
|
pub trigger_value: f64,
|
|
pub threshold: f64,
|
|
pub reset_time: Option<DateTime<Utc>>,
|
|
pub trigger_count: u64,
|
|
}
|
|
|
|
impl CircuitBreakerState {
|
|
pub fn new(breaker_type: CircuitBreakerType, threshold: f64) -> Self {
|
|
Self {
|
|
breaker_type,
|
|
is_triggered: false,
|
|
trigger_time: None,
|
|
trigger_value: 0.0,
|
|
threshold,
|
|
reset_time: None,
|
|
trigger_count: 0,
|
|
}
|
|
}
|
|
|
|
pub fn trigger(&mut self, value: f64) {
|
|
self.is_triggered = true;
|
|
self.trigger_time = Some(Utc::now());
|
|
self.trigger_value = value;
|
|
self.trigger_count += 1;
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
self.is_triggered = false;
|
|
self.reset_time = Some(Utc::now());
|
|
self.trigger_value = 0.0;
|
|
}
|
|
|
|
pub fn can_reset(&self, cooldown_seconds: u64) -> bool {
|
|
if let Some(trigger_time) = self.trigger_time {
|
|
let elapsed = Utc::now().signed_duration_since(trigger_time);
|
|
elapsed.num_seconds() >= cooldown_seconds as i64
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ML Circuit Breaker system
|
|
#[derive(Debug)]
|
|
pub struct MLCircuitBreaker {
|
|
config: CircuitBreakerConfig,
|
|
states: HashMap<CircuitBreakerType, CircuitBreakerState>,
|
|
historical_data: VecDeque<MarketDataPoint>,
|
|
performance_history: VecDeque<f64>,
|
|
}
|
|
|
|
/// Market data point for circuit breaker analysis
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MarketDataPoint {
|
|
pub timestamp: DateTime<Utc>,
|
|
pub price: Price,
|
|
pub volume: Volume,
|
|
pub volatility: f64,
|
|
pub returns: f64,
|
|
}
|
|
|
|
impl MLCircuitBreaker {
|
|
pub fn new(config: CircuitBreakerConfig) -> Result<Self> {
|
|
let mut states = HashMap::new();
|
|
|
|
// Initialize all circuit breaker states
|
|
states.insert(
|
|
CircuitBreakerType::Volatility,
|
|
CircuitBreakerState::new(CircuitBreakerType::Volatility, config.volatility_threshold),
|
|
);
|
|
states.insert(
|
|
CircuitBreakerType::Drawdown,
|
|
CircuitBreakerState::new(CircuitBreakerType::Drawdown, config.drawdown_threshold),
|
|
);
|
|
states.insert(
|
|
CircuitBreakerType::Volume,
|
|
CircuitBreakerState::new(CircuitBreakerType::Volume, config.volume_spike_threshold),
|
|
);
|
|
states.insert(
|
|
CircuitBreakerType::ModelPerformance,
|
|
CircuitBreakerState::new(
|
|
CircuitBreakerType::ModelPerformance,
|
|
config.model_accuracy_threshold,
|
|
),
|
|
);
|
|
states.insert(
|
|
CircuitBreakerType::MarketStress,
|
|
CircuitBreakerState::new(CircuitBreakerType::MarketStress, 0.95), // 95th percentile
|
|
);
|
|
states.insert(
|
|
CircuitBreakerType::PositionConcentration,
|
|
CircuitBreakerState::new(
|
|
CircuitBreakerType::PositionConcentration,
|
|
config.max_position_size,
|
|
),
|
|
);
|
|
|
|
let lookback_period = config.lookback_period;
|
|
Ok(Self {
|
|
config,
|
|
states,
|
|
historical_data: VecDeque::with_capacity(lookback_period),
|
|
performance_history: VecDeque::with_capacity(100),
|
|
})
|
|
}
|
|
|
|
/// Update circuit breaker with new market data
|
|
pub fn update_market_data(&mut self, data: MarketDataPoint) -> Result<Vec<CircuitBreakerType>> {
|
|
let mut triggered_breakers = Vec::new();
|
|
|
|
// Add to historical data
|
|
self.historical_data.push_back(data.clone());
|
|
if self.historical_data.len() > self.config.lookback_period {
|
|
self.historical_data.pop_front();
|
|
}
|
|
|
|
// Check volatility circuit breaker
|
|
if data.volatility > self.config.volatility_threshold {
|
|
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Volatility) {
|
|
if !state.is_triggered {
|
|
state.trigger(data.volatility);
|
|
triggered_breakers.push(CircuitBreakerType::Volatility);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check volume circuit breaker
|
|
if self.historical_data.len() > 10 {
|
|
let avg_volume = self
|
|
.historical_data
|
|
.iter()
|
|
.take(self.historical_data.len() - 1)
|
|
.map(|d| d.volume.to_f64())
|
|
.sum::<f64>()
|
|
/ (self.historical_data.len() - 1) as f64;
|
|
|
|
let volume_ratio = data.volume.to_f64() / avg_volume;
|
|
if volume_ratio > self.config.volume_spike_threshold {
|
|
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Volume) {
|
|
if !state.is_triggered {
|
|
state.trigger(volume_ratio);
|
|
triggered_breakers.push(CircuitBreakerType::Volume);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check drawdown circuit breaker
|
|
if let Some(max_price) =
|
|
self.historical_data
|
|
.iter()
|
|
.map(|d| d.price.to_f64())
|
|
.fold(None, |max, price| match max {
|
|
None => Some(price),
|
|
Some(m) => Some(m.max(price)),
|
|
})
|
|
{
|
|
let current_drawdown = (max_price - data.price.to_f64()) / max_price;
|
|
if current_drawdown > self.config.drawdown_threshold {
|
|
if let Some(state) = self.states.get_mut(&CircuitBreakerType::Drawdown) {
|
|
if !state.is_triggered {
|
|
state.trigger(current_drawdown);
|
|
triggered_breakers.push(CircuitBreakerType::Drawdown);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(triggered_breakers)
|
|
}
|
|
|
|
/// Update model performance and check performance circuit breaker
|
|
pub fn update_model_performance(&mut self, accuracy: f64) -> Result<bool> {
|
|
self.performance_history.push_back(accuracy);
|
|
if self.performance_history.len() > 100 {
|
|
self.performance_history.pop_front();
|
|
}
|
|
|
|
if accuracy < self.config.model_accuracy_threshold {
|
|
if let Some(state) = self.states.get_mut(&CircuitBreakerType::ModelPerformance) {
|
|
if !state.is_triggered {
|
|
state.trigger(accuracy);
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Check if any circuit breakers are triggered
|
|
pub fn is_any_triggered(&self) -> bool {
|
|
self.states.values().any(|state| state.is_triggered)
|
|
}
|
|
|
|
/// Check if specific circuit breaker is triggered
|
|
pub fn is_triggered(&self, breaker_type: CircuitBreakerType) -> bool {
|
|
self.states
|
|
.get(&breaker_type)
|
|
.map(|state| state.is_triggered)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Get all triggered circuit breakers
|
|
pub fn get_triggered_breakers(&self) -> Vec<CircuitBreakerType> {
|
|
self.states
|
|
.iter()
|
|
.filter_map(|(breaker_type, state)| state.is_triggered.then_some(*breaker_type))
|
|
.collect()
|
|
}
|
|
|
|
/// Reset all circuit breakers that can be reset
|
|
pub fn reset_eligible_breakers(&mut self) -> Vec<CircuitBreakerType> {
|
|
let mut reset_breakers = Vec::new();
|
|
|
|
for (breaker_type, state) in self.states.iter_mut() {
|
|
if state.is_triggered && state.can_reset(self.config.cooldown_seconds) {
|
|
state.reset();
|
|
reset_breakers.push(*breaker_type);
|
|
}
|
|
}
|
|
|
|
reset_breakers
|
|
}
|
|
|
|
/// Manually reset specific circuit breaker
|
|
pub fn reset_breaker(&mut self, breaker_type: CircuitBreakerType) -> Result<()> {
|
|
if let Some(state) = self.states.get_mut(&breaker_type) {
|
|
state.reset();
|
|
Ok(())
|
|
} else {
|
|
Err(MLError::InvalidInput(format!(
|
|
"Unknown circuit breaker type: {:?}",
|
|
breaker_type
|
|
)))
|
|
}
|
|
}
|
|
|
|
/// Get circuit breaker state
|
|
pub fn get_state(&self, breaker_type: CircuitBreakerType) -> Option<&CircuitBreakerState> {
|
|
self.states.get(&breaker_type)
|
|
}
|
|
|
|
/// Get all circuit breaker states
|
|
pub fn get_all_states(&self) -> &HashMap<CircuitBreakerType, CircuitBreakerState> {
|
|
&self.states
|
|
}
|
|
|
|
/// Calculate market stress score
|
|
pub fn calculate_market_stress(&self) -> f64 {
|
|
if self.historical_data.len() < 10 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Calculate volatility score
|
|
let volatilities: Vec<f64> = self.historical_data.iter().map(|d| d.volatility).collect();
|
|
let avg_volatility = volatilities.iter().sum::<f64>() / volatilities.len() as f64;
|
|
let volatility_score = (avg_volatility / self.config.volatility_threshold).min(1.0);
|
|
|
|
// Calculate volume score
|
|
let volumes: Vec<f64> = self
|
|
.historical_data
|
|
.iter()
|
|
.map(|d| d.volume.to_f64())
|
|
.collect();
|
|
let avg_volume = volumes.iter().sum::<f64>() / volumes.len() as f64;
|
|
let recent_volume = volumes.iter().rev().take(5).sum::<f64>() / 5.0;
|
|
let volume_score =
|
|
(recent_volume / avg_volume / self.config.volume_spike_threshold).min(1.0);
|
|
|
|
// Calculate returns volatility
|
|
let returns: Vec<f64> = self.historical_data.iter().map(|d| d.returns).collect();
|
|
let returns_std = {
|
|
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()
|
|
};
|
|
let returns_score = (returns_std / 0.02).min(1.0); // 2% daily volatility baseline
|
|
|
|
// Combined stress score
|
|
volatility_score * 0.4 + volume_score * 0.3 + returns_score * 0.3
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
|
|
#[test]
|
|
fn test_circuit_breaker_creation() -> Result<()> {
|
|
let config = CircuitBreakerConfig::default();
|
|
let breaker = MLCircuitBreaker::new(config);
|
|
assert!(breaker.is_ok());
|
|
|
|
let breaker = breaker?;
|
|
assert!(!breaker.is_any_triggered());
|
|
assert_eq!(breaker.get_triggered_breakers().len(), 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_breaker_state() {
|
|
let mut state = CircuitBreakerState::new(CircuitBreakerType::Volatility, 0.05);
|
|
|
|
assert!(!state.is_triggered);
|
|
assert_eq!(state.trigger_count, 0);
|
|
|
|
state.trigger(0.08);
|
|
assert!(state.is_triggered);
|
|
assert_eq!(state.trigger_count, 1);
|
|
assert_eq!(state.trigger_value, 0.08);
|
|
|
|
state.reset();
|
|
assert!(!state.is_triggered);
|
|
assert_eq!(state.trigger_value, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_volatility_circuit_breaker() -> Result<()> {
|
|
let config = CircuitBreakerConfig::default();
|
|
let mut breaker = MLCircuitBreaker::new(config)?;
|
|
|
|
// Normal volatility - should not trigger
|
|
let normal_data = MarketDataPoint {
|
|
timestamp: Utc::now(),
|
|
price: Price::from_f64(100.0).unwrap(),
|
|
volume: Volume::from_f64(1000.0).unwrap(),
|
|
volatility: 0.02, // 2% - below threshold
|
|
returns: 0.01,
|
|
};
|
|
|
|
let triggered = breaker.update_market_data(normal_data)?;
|
|
assert!(triggered.is_empty());
|
|
assert!(!breaker.is_any_triggered());
|
|
|
|
// High volatility - should trigger
|
|
let high_vol_data = MarketDataPoint {
|
|
timestamp: Utc::now(),
|
|
price: Price::from_f64(105.0).unwrap(),
|
|
volume: Volume::from_f64(1000.0).unwrap(),
|
|
volatility: 0.08, // 8% - above 5% threshold
|
|
returns: 0.05,
|
|
};
|
|
|
|
let triggered = breaker.update_market_data(high_vol_data)?;
|
|
assert!(!triggered.is_empty());
|
|
assert!(triggered.contains(&CircuitBreakerType::Volatility));
|
|
assert!(breaker.is_triggered(CircuitBreakerType::Volatility));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_performance_circuit_breaker() -> Result<()> {
|
|
let config = CircuitBreakerConfig::default();
|
|
let mut breaker = MLCircuitBreaker::new(config)?;
|
|
|
|
// Good performance - should not trigger
|
|
let good_triggered = breaker.update_model_performance(0.95)?;
|
|
assert!(!good_triggered);
|
|
assert!(!breaker.is_triggered(CircuitBreakerType::ModelPerformance));
|
|
|
|
// Poor performance - should trigger
|
|
let poor_triggered = breaker.update_model_performance(0.60)?; // Below 80% threshold
|
|
assert!(poor_triggered);
|
|
assert!(breaker.is_triggered(CircuitBreakerType::ModelPerformance));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_breaker_reset() -> Result<()> {
|
|
let config = CircuitBreakerConfig::default();
|
|
let mut breaker = MLCircuitBreaker::new(config)?;
|
|
|
|
// Trigger a circuit breaker
|
|
breaker.update_model_performance(0.60)?;
|
|
assert!(breaker.is_triggered(CircuitBreakerType::ModelPerformance));
|
|
|
|
// Manual reset
|
|
breaker.reset_breaker(CircuitBreakerType::ModelPerformance)?;
|
|
assert!(!breaker.is_triggered(CircuitBreakerType::ModelPerformance));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_market_stress_calculation() -> Result<()> {
|
|
let config = CircuitBreakerConfig::default();
|
|
let mut breaker = MLCircuitBreaker::new(config)?;
|
|
|
|
// Add some historical data
|
|
for i in 0..20 {
|
|
let data = MarketDataPoint {
|
|
timestamp: Utc::now(),
|
|
price: Price::from_f64(100.0 + i as f64).unwrap(),
|
|
volume: Volume::from_f64(1000.0).unwrap(),
|
|
volatility: 0.02,
|
|
returns: 0.01,
|
|
};
|
|
breaker.update_market_data(data)?;
|
|
}
|
|
|
|
let stress_score = breaker.calculate_market_stress();
|
|
assert!(stress_score >= 0.0 && stress_score <= 1.0);
|
|
Ok(())
|
|
}
|
|
}
|