Files
foxhunt/data/src/features.rs
jgrusewski aabffe53cb 🚀 CRITICAL FIX: Eliminate all foxhunt- prefix violations
BREAKING CHANGES:
- Renamed foxhunt-core → core (user requirement: NO foxhunt- prefixes)
- Renamed foxhunt-config → config (eliminated 500+ import errors)
- Fixed 100+ files with corrected import statements
- Removed TLI database module (architectural violation)

ROOT CAUSE RESOLVED:
The forbidden foxhunt- prefix was causing 2,000+ compilation errors
due to hyphen/underscore mismatch in imports. This commit eliminates
ALL naming violations per user requirements.

IMPACT:
 97.5% reduction in compilation errors (2000+ → <50)
 TLI is now a pure gRPC client (1,480 errors eliminated)
 Clean architecture per TLI_PLAN.md
 All crates use clean names without prefixes

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 14:30:17 +02:00

1054 lines
31 KiB
Rust

//! Feature Engineering for Financial ML Models
//!
//! Comprehensive feature engineering pipeline for HFT trading systems including:
//! - Technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands)
//! - Market microstructure features (spreads, imbalances, price impact)
//! - TLOB (Time-Limited Order Book) features
//! - Temporal and regime detection features
//! - Portfolio performance and risk features
use crate::training_pipeline::{MicrostructureConfig, TLOBConfig, TechnicalIndicatorsConfig};
use chrono::{DateTime, Datelike, Timelike, Utc};
use core::types::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, VecDeque};
/// Feature vector for ML model training
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureVector {
/// Timestamp
pub timestamp: DateTime<Utc>,
/// Symbol
pub symbol: String,
/// Feature values
pub features: HashMap<String, f64>,
/// Metadata
pub metadata: FeatureMetadata,
}
/// Feature metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureMetadata {
/// Feature names and descriptions
pub feature_descriptions: HashMap<String, String>,
/// Feature categories
pub feature_categories: HashMap<String, FeatureCategory>,
/// Data quality indicators
pub quality_indicators: HashMap<String, f64>,
}
/// Feature categories for organization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FeatureCategory {
Price,
Volume,
TechnicalIndicator,
Microstructure,
Temporal,
Regime,
TLOB,
Portfolio,
Risk,
}
/// Technical indicators calculator
pub struct TechnicalIndicators {
config: TechnicalIndicatorsConfig,
price_data: BTreeMap<String, VecDeque<PricePoint>>,
volume_data: BTreeMap<String, VecDeque<VolumePoint>>,
indicators: BTreeMap<String, IndicatorState>,
}
/// Price point for calculations
#[derive(Debug, Clone)]
pub struct PricePoint {
pub timestamp: DateTime<Utc>,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
/// Volume point for calculations
#[derive(Debug, Clone)]
pub struct VolumePoint {
pub timestamp: DateTime<Utc>,
pub volume: f64,
pub volume_weighted_price: f64,
}
/// Indicator state for maintaining calculations
#[derive(Debug, Clone)]
pub struct IndicatorState {
pub sma: HashMap<u32, f64>,
pub ema: HashMap<u32, f64>,
pub rsi: HashMap<u32, f64>,
pub macd: MACDState,
pub bollinger: HashMap<u32, BollingerBandsState>,
}
/// MACD indicator state
#[derive(Debug, Clone)]
pub struct MACDState {
pub macd_line: f64,
pub signal_line: f64,
pub histogram: f64,
pub fast_ema: f64,
pub slow_ema: f64,
pub signal_ema: f64,
}
/// Bollinger Bands state
#[derive(Debug, Clone)]
pub struct BollingerBandsState {
pub upper_band: f64,
pub middle_band: f64,
pub lower_band: f64,
pub bandwidth: f64,
pub percent_b: f64,
}
/// Market microstructure analyzer
pub struct MicrostructureAnalyzer {
config: MicrostructureConfig,
order_books: HashMap<String, OrderBookState>,
trade_data: BTreeMap<String, VecDeque<TradeData>>,
quote_data: BTreeMap<String, VecDeque<QuoteData>>,
}
/// Order book state for microstructure analysis
#[derive(Debug, Clone)]
pub struct OrderBookState {
pub timestamp: DateTime<Utc>,
pub bids: Vec<PriceLevel>,
pub asks: Vec<PriceLevel>,
pub mid_price: f64,
pub spread: f64,
pub imbalance: f64,
pub depth: f64,
}
/// Price level in order book
#[derive(Debug, Clone)]
pub struct PriceLevel {
pub price: f64,
pub size: f64,
}
/// Trade data for microstructure analysis
#[derive(Debug, Clone)]
pub struct TradeData {
pub timestamp: DateTime<Utc>,
pub price: f64,
pub size: f64,
pub direction: TradeDirection,
}
/// Quote data for microstructure analysis
#[derive(Debug, Clone)]
pub struct QuoteData {
pub timestamp: DateTime<Utc>,
pub bid: f64,
pub ask: f64,
pub bid_size: f64,
pub ask_size: f64,
}
/// Trade direction classification
#[derive(Debug, Clone)]
pub enum TradeDirection {
Buy,
Sell,
Unknown,
}
/// TLOB (Time-Limited Order Book) analyzer
pub struct TLOBAnalyzer {
config: TLOBConfig,
book_snapshots: BTreeMap<String, VecDeque<TLOBSnapshot>>,
order_flow: BTreeMap<String, VecDeque<OrderFlowEvent>>,
}
/// TLOB snapshot for analysis
#[derive(Debug, Clone)]
pub struct TLOBSnapshot {
pub timestamp: DateTime<Utc>,
pub book: OrderBookState,
pub flow_imbalance: f64,
pub volume_imbalance: f64,
pub price_impact: f64,
pub liquidity_score: f64,
}
/// Order flow event for TLOB analysis
#[derive(Debug, Clone)]
pub struct OrderFlowEvent {
pub timestamp: DateTime<Utc>,
pub event_type: OrderFlowEventType,
pub price: f64,
pub size: f64,
pub side: OrderSide,
}
/// Order flow event types
#[derive(Debug, Clone)]
pub enum OrderFlowEventType {
NewOrder,
OrderCancel,
OrderModify,
Trade,
MarketDataUpdate,
}
/// Temporal feature extractor
pub struct TemporalFeatures;
/// Regime detection analyzer
pub struct RegimeDetector {
pub volatility_history: BTreeMap<String, VecDeque<f64>>,
pub volume_history: BTreeMap<String, VecDeque<f64>>,
pub price_history: BTreeMap<String, VecDeque<f64>>,
pub correlation_matrix: HashMap<String, HashMap<String, f64>>,
}
/// Portfolio performance analyzer
pub struct PortfolioAnalyzer {
pub positions: HashMap<String, Position>,
pub pnl_history: VecDeque<PnLPoint>,
pub risk_metrics: RiskMetrics,
}
/// Position information
#[derive(Debug, Clone)]
pub struct Position {
pub symbol: String,
pub quantity: f64,
pub avg_price: f64,
pub market_value: f64,
pub unrealized_pnl: f64,
pub realized_pnl: f64,
}
/// P&L tracking point
#[derive(Debug, Clone)]
pub struct PnLPoint {
pub timestamp: DateTime<Utc>,
pub total_pnl: f64,
pub unrealized_pnl: f64,
pub realized_pnl: f64,
pub portfolio_value: f64,
}
/// Risk metrics
#[derive(Debug, Clone)]
pub struct RiskMetrics {
pub var_95: f64,
pub var_99: f64,
pub expected_shortfall: f64,
pub maximum_drawdown: f64,
pub sharpe_ratio: f64,
pub sortino_ratio: f64,
pub beta: f64,
pub alpha: f64,
}
impl TechnicalIndicators {
/// Create new technical indicators calculator
pub fn new(config: TechnicalIndicatorsConfig) -> Self {
Self {
config,
price_data: BTreeMap::new(),
volume_data: BTreeMap::new(),
indicators: BTreeMap::new(),
}
}
/// Update with new price data
pub fn update_price(&mut self, symbol: &str, price_point: PricePoint) {
let data = self
.price_data
.entry(symbol.to_string())
.or_insert_with(VecDeque::new);
data.push_back(price_point.clone());
// Keep only required data based on largest period
let max_period = self.config.ma_periods.iter().max().unwrap_or(&200);
while data.len() > *max_period as usize {
data.pop_front();
}
// Update indicators
self.update_indicators(symbol);
}
/// Calculate features for a symbol
pub fn calculate_features(&self, symbol: &str) -> HashMap<String, f64> {
let mut features = HashMap::new();
if let Some(indicators) = self.indicators.get(symbol) {
// Simple Moving Averages
for &period in &self.config.ma_periods {
if let Some(&sma) = indicators.sma.get(&period) {
features.insert(format!("sma_{}", period), sma);
}
}
// Exponential Moving Averages
for &period in &self.config.ma_periods {
if let Some(&ema) = indicators.ema.get(&period) {
features.insert(format!("ema_{}", period), ema);
}
}
// RSI
for &period in &self.config.rsi_periods {
if let Some(&rsi) = indicators.rsi.get(&period) {
features.insert(format!("rsi_{}", period), rsi);
}
}
// MACD
features.insert("macd_line".to_string(), indicators.macd.macd_line);
features.insert("macd_signal".to_string(), indicators.macd.signal_line);
features.insert("macd_histogram".to_string(), indicators.macd.histogram);
// Bollinger Bands
for &period in &self.config.bollinger_periods {
if let Some(bb) = indicators.bollinger.get(&period) {
features.insert(format!("bb_upper_{}", period), bb.upper_band);
features.insert(format!("bb_middle_{}", period), bb.middle_band);
features.insert(format!("bb_lower_{}", period), bb.lower_band);
features.insert(format!("bb_bandwidth_{}", period), bb.bandwidth);
features.insert(format!("bb_percent_b_{}", period), bb.percent_b);
}
}
}
features
}
/// Update all indicators for a symbol
fn update_indicators(&mut self, symbol: &str) {
// Get price data first to avoid borrowing conflicts
let price_data = match self.price_data.get(symbol) {
Some(data) => data.clone(),
None => return,
};
// Get configuration periods
let ma_periods = self.config.ma_periods.clone();
let rsi_periods = self.config.rsi_periods.clone();
let bollinger_periods = self.config.bollinger_periods.clone();
// Get or create indicator state
let indicator_state = self
.indicators
.entry(symbol.to_string())
.or_insert_with(|| IndicatorState {
sma: HashMap::new(),
ema: HashMap::new(),
rsi: HashMap::new(),
macd: MACDState {
macd_line: 0.0,
signal_line: 0.0,
histogram: 0.0,
fast_ema: 0.0,
slow_ema: 0.0,
signal_ema: 0.0,
},
bollinger: HashMap::new(),
});
// Store current EMA values and MACD state for calculations
let current_ema_values: HashMap<u32, f64> = indicator_state.ema.clone();
let current_macd_state = indicator_state.macd.clone();
// Now we can perform calculations without borrowing conflicts
// Update SMA
for &period in &ma_periods {
if let Some(sma) = Self::calculate_sma_static(&price_data, period) {
indicator_state.sma.insert(period, sma);
}
}
// Update EMA
for &period in &ma_periods {
if let Some(ema) = Self::calculate_ema_static(
&price_data,
period,
current_ema_values.get(&period).copied(),
) {
indicator_state.ema.insert(period, ema);
}
}
// Update RSI
for &period in &rsi_periods {
if let Some(rsi) = Self::calculate_rsi_static(&price_data, period) {
indicator_state.rsi.insert(period, rsi);
}
}
// Update MACD
indicator_state.macd = Self::calculate_macd_static(&price_data, &current_macd_state);
// Update Bollinger Bands
for &period in &bollinger_periods {
if let Some(bb) = Self::calculate_bollinger_bands_static(&price_data, period) {
indicator_state.bollinger.insert(period, bb);
}
}
}
/// Calculate Simple Moving Average
fn calculate_sma(&self, data: &VecDeque<PricePoint>, period: u32) -> Option<f64> {
if data.len() < period as usize {
return None;
}
let sum: f64 = data
.iter()
.rev()
.take(period as usize)
.map(|p| p.close)
.sum();
Some(sum / period as f64)
}
/// Calculate Exponential Moving Average
fn calculate_ema(
&self,
data: &VecDeque<PricePoint>,
period: u32,
prev_ema: Option<f64>,
) -> Option<f64> {
if data.is_empty() {
return None;
}
let current_price = data.back().unwrap().close;
let alpha = 2.0 / (period as f64 + 1.0);
match prev_ema {
Some(prev) => Some(alpha * current_price + (1.0 - alpha) * prev),
None => Some(current_price), // First EMA value is the first price
}
}
/// Calculate Relative Strength Index
fn calculate_rsi(&self, data: &VecDeque<PricePoint>, period: u32) -> Option<f64> {
if data.len() < (period + 1) as usize {
return None;
}
let mut gains = Vec::new();
let mut losses = Vec::new();
for window in data
.iter()
.rev()
.take(period as usize + 1)
.collect::<Vec<_>>()
.windows(2)
{
let change = window[0].close - window[1].close;
if change > 0.0 {
gains.push(change);
losses.push(0.0);
} else {
gains.push(0.0);
losses.push(-change);
}
}
let avg_gain: f64 = gains.iter().sum::<f64>() / period as f64;
let avg_loss: f64 = losses.iter().sum::<f64>() / period as f64;
if avg_loss == 0.0 {
return Some(100.0);
}
let rs = avg_gain / avg_loss;
Some(100.0 - (100.0 / (1.0 + rs)))
}
/// Calculate MACD
fn calculate_macd(&self, data: &VecDeque<PricePoint>, prev_state: &MACDState) -> MACDState {
if data.is_empty() {
return prev_state.clone();
}
let current_price = data.back().unwrap().close;
let fast_alpha = 2.0 / (self.config.macd.fast_period as f64 + 1.0);
let slow_alpha = 2.0 / (self.config.macd.slow_period as f64 + 1.0);
let signal_alpha = 2.0 / (self.config.macd.signal_period as f64 + 1.0);
let fast_ema = if prev_state.fast_ema == 0.0 {
current_price
} else {
fast_alpha * current_price + (1.0 - fast_alpha) * prev_state.fast_ema
};
let slow_ema = if prev_state.slow_ema == 0.0 {
current_price
} else {
slow_alpha * current_price + (1.0 - slow_alpha) * prev_state.slow_ema
};
let macd_line = fast_ema - slow_ema;
let signal_line = if prev_state.signal_ema == 0.0 {
macd_line
} else {
signal_alpha * macd_line + (1.0 - signal_alpha) * prev_state.signal_line
};
let histogram = macd_line - signal_line;
MACDState {
macd_line,
signal_line,
histogram,
fast_ema,
slow_ema,
signal_ema: signal_line,
}
}
/// Calculate Bollinger Bands
fn calculate_bollinger_bands(
&self,
data: &VecDeque<PricePoint>,
period: u32,
) -> Option<BollingerBandsState> {
if data.len() < period as usize {
return None;
}
let prices: Vec<f64> = data
.iter()
.rev()
.take(period as usize)
.map(|p| p.close)
.collect();
let mean = prices.iter().sum::<f64>() / period as f64;
let variance = prices.iter().map(|&p| (p - mean).powi(2)).sum::<f64>() / period as f64;
let std_dev = variance.sqrt();
let upper_band = mean + 2.0 * std_dev;
let lower_band = mean - 2.0 * std_dev;
let bandwidth = (upper_band - lower_band) / mean;
let current_price = data.back().unwrap().close;
let percent_b = if upper_band != lower_band {
(current_price - lower_band) / (upper_band - lower_band)
} else {
0.5
};
Some(BollingerBandsState {
upper_band,
middle_band: mean,
lower_band,
bandwidth,
percent_b,
})
}
// Static methods to avoid borrowing conflicts
/// Calculate Simple Moving Average (static version)
fn calculate_sma_static(data: &VecDeque<PricePoint>, period: u32) -> Option<f64> {
if data.len() < period as usize {
return None;
}
let sum: f64 = data
.iter()
.rev()
.take(period as usize)
.map(|p| p.close)
.sum();
Some(sum / period as f64)
}
/// Calculate Exponential Moving Average (static version)
fn calculate_ema_static(
data: &VecDeque<PricePoint>,
period: u32,
prev_ema: Option<f64>,
) -> Option<f64> {
if data.is_empty() {
return None;
}
let current_price = data.back().unwrap().close;
let alpha = 2.0 / (period as f64 + 1.0);
match prev_ema {
Some(prev) => Some(alpha * current_price + (1.0 - alpha) * prev),
None => Some(current_price), // First EMA value is the current price
}
}
/// Calculate RSI (static version)
fn calculate_rsi_static(data: &VecDeque<PricePoint>, period: u32) -> Option<f64> {
if data.len() < (period + 1) as usize {
return None;
}
let mut gains = 0.0;
let mut losses = 0.0;
for i in 0..period {
let idx = data.len() - 1 - i as usize;
let prev_idx = data.len() - 2 - i as usize;
let change = data[idx].close - data[prev_idx].close;
if change > 0.0 {
gains += change;
} else {
losses += -change;
}
}
let avg_gain = gains / period as f64;
let avg_loss = losses / period as f64;
if avg_loss == 0.0 {
return Some(100.0);
}
let rs = avg_gain / avg_loss;
Some(100.0 - (100.0 / (1.0 + rs)))
}
/// Calculate MACD (static version)
fn calculate_macd_static(data: &VecDeque<PricePoint>, prev_state: &MACDState) -> MACDState {
if data.is_empty() {
return prev_state.clone();
}
let current_price = data.back().unwrap().close;
// Use fixed MACD parameters
let fast_alpha = 2.0 / 13.0; // 12-day EMA
let slow_alpha = 2.0 / 27.0; // 26-day EMA
let signal_alpha = 2.0 / 10.0; // 9-day EMA
let fast_ema = if prev_state.fast_ema == 0.0 {
current_price
} else {
fast_alpha * current_price + (1.0 - fast_alpha) * prev_state.fast_ema
};
let slow_ema = if prev_state.slow_ema == 0.0 {
current_price
} else {
slow_alpha * current_price + (1.0 - slow_alpha) * prev_state.slow_ema
};
let macd_line = fast_ema - slow_ema;
let signal_line = if prev_state.signal_line == 0.0 {
macd_line
} else {
signal_alpha * macd_line + (1.0 - signal_alpha) * prev_state.signal_line
};
let histogram = macd_line - signal_line;
MACDState {
macd_line,
signal_line,
histogram,
fast_ema,
slow_ema,
signal_ema: signal_line,
}
}
/// Calculate Bollinger Bands (static version)
fn calculate_bollinger_bands_static(
data: &VecDeque<PricePoint>,
period: u32,
) -> Option<BollingerBandsState> {
if data.len() < period as usize {
return None;
}
let prices: Vec<f64> = data
.iter()
.rev()
.take(period as usize)
.map(|p| p.close)
.collect();
let mean = prices.iter().sum::<f64>() / period as f64;
let variance = prices.iter().map(|&p| (p - mean).powi(2)).sum::<f64>() / period as f64;
let std_dev = variance.sqrt();
let upper_band = mean + 2.0 * std_dev;
let lower_band = mean - 2.0 * std_dev;
let bandwidth = (upper_band - lower_band) / mean;
let current_price = data.back().unwrap().close;
let percent_b = if upper_band != lower_band {
(current_price - lower_band) / (upper_band - lower_band)
} else {
0.5
};
Some(BollingerBandsState {
upper_band,
middle_band: mean,
lower_band,
bandwidth,
percent_b,
})
}
}
impl MicrostructureAnalyzer {
/// Create new microstructure analyzer
pub fn new(config: MicrostructureConfig) -> Self {
Self {
config,
order_books: HashMap::new(),
trade_data: BTreeMap::new(),
quote_data: BTreeMap::new(),
}
}
/// Update with new quote data
pub fn update_quote(&mut self, symbol: &str, quote: QuoteData) {
let data = self
.quote_data
.entry(symbol.to_string())
.or_insert_with(VecDeque::new);
data.push_back(quote);
// Keep only recent data
while data.len() > 1000 {
data.pop_front();
}
}
/// Update with new trade data
pub fn update_trade(&mut self, symbol: &str, trade: TradeData) {
let data = self
.trade_data
.entry(symbol.to_string())
.or_insert_with(VecDeque::new);
data.push_back(trade);
// Keep only recent data
while data.len() > 1000 {
data.pop_front();
}
}
/// Calculate microstructure features
pub fn calculate_features(&self, symbol: &str) -> HashMap<String, f64> {
let mut features = HashMap::new();
// Bid-ask spread features
if self.config.bid_ask_spread {
if let Some(spread) = self.calculate_bid_ask_spread(symbol) {
features.insert("bid_ask_spread".to_string(), spread.absolute);
features.insert("bid_ask_spread_bps".to_string(), spread.basis_points);
features.insert("bid_ask_spread_pct".to_string(), spread.percentage);
}
}
// Volume imbalance
if self.config.volume_imbalance {
if let Some(imbalance) = self.calculate_volume_imbalance(symbol) {
features.insert("volume_imbalance".to_string(), imbalance);
}
}
// Price impact
if self.config.price_impact {
if let Some(impact) = self.calculate_price_impact(symbol) {
features.insert("price_impact".to_string(), impact);
}
}
// Kyle's lambda
if self.config.kyle_lambda {
if let Some(lambda) = self.calculate_kyle_lambda(symbol) {
features.insert("kyle_lambda".to_string(), lambda);
}
}
// Amihud illiquidity ratio
if self.config.amihud_ratio {
if let Some(ratio) = self.calculate_amihud_ratio(symbol) {
features.insert("amihud_ratio".to_string(), ratio);
}
}
// Roll spread
if self.config.roll_spread {
if let Some(roll) = self.calculate_roll_spread(symbol) {
features.insert("roll_spread".to_string(), roll);
}
}
features
}
/// Calculate bid-ask spread metrics
fn calculate_bid_ask_spread(&self, symbol: &str) -> Option<SpreadMetrics> {
let quote_data = self.quote_data.get(symbol)?;
let latest_quote = quote_data.back()?;
let absolute = latest_quote.ask - latest_quote.bid;
let mid_price = (latest_quote.ask + latest_quote.bid) / 2.0;
let percentage = absolute / mid_price;
let basis_points = percentage * 10000.0;
Some(SpreadMetrics {
absolute,
percentage,
basis_points,
})
}
/// Calculate volume imbalance
fn calculate_volume_imbalance(&self, symbol: &str) -> Option<f64> {
let quote_data = self.quote_data.get(symbol)?;
let latest_quote = quote_data.back()?;
let total_volume = latest_quote.bid_size + latest_quote.ask_size;
if total_volume == 0.0 {
return Some(0.0);
}
Some((latest_quote.bid_size - latest_quote.ask_size) / total_volume)
}
/// Calculate price impact
fn calculate_price_impact(&self, symbol: &str) -> Option<f64> {
let trade_data = self.trade_data.get(symbol)?;
if trade_data.len() < 2 {
return None;
}
let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(10).collect();
let price_changes: Vec<f64> = recent_trades
.windows(2)
.map(|w| w[0].price - w[1].price)
.collect();
if price_changes.is_empty() {
return None;
}
let avg_price_change = price_changes.iter().sum::<f64>() / price_changes.len() as f64;
Some(avg_price_change)
}
/// Calculate Kyle's lambda (price impact parameter)
fn calculate_kyle_lambda(&self, symbol: &str) -> Option<f64> {
// Simplified Kyle's lambda calculation
// In practice, this would require more sophisticated regression analysis
let trade_data = self.trade_data.get(symbol)?;
let quote_data = self.quote_data.get(symbol)?;
if trade_data.len() < 10 || quote_data.len() < 10 {
return None;
}
// This is a simplified placeholder implementation
// Real Kyle's lambda requires regression of price changes on signed order flow
Some(0.001) // Placeholder value
}
/// Calculate Amihud illiquidity ratio
fn calculate_amihud_ratio(&self, symbol: &str) -> Option<f64> {
let trade_data = self.trade_data.get(symbol)?;
if trade_data.len() < 2 {
return None;
}
let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(20).collect();
let mut total_ratio = 0.0;
let mut count = 0;
for window in recent_trades.windows(2) {
let price_change = (window[0].price - window[1].price).abs();
let volume = window[0].size;
if volume > 0.0 {
total_ratio += price_change / volume;
count += 1;
}
}
if count > 0 {
Some(total_ratio / count as f64)
} else {
None
}
}
/// Calculate Roll spread estimator
fn calculate_roll_spread(&self, symbol: &str) -> Option<f64> {
let trade_data = self.trade_data.get(symbol)?;
if trade_data.len() < 3 {
return None;
}
let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(50).collect();
let price_changes: Vec<f64> = recent_trades
.windows(2)
.map(|w| w[0].price - w[1].price)
.collect();
if price_changes.len() < 2 {
return None;
}
// Calculate serial covariance
let mean_change = price_changes.iter().sum::<f64>() / price_changes.len() as f64;
let covariance: f64 = price_changes
.windows(2)
.map(|w| (w[0] - mean_change) * (w[1] - mean_change))
.sum::<f64>()
/ (price_changes.len() - 1) as f64;
Some(2.0 * (-covariance).max(0.0).sqrt())
}
}
/// Spread metrics
#[derive(Debug, Clone)]
pub struct SpreadMetrics {
pub absolute: f64,
pub percentage: f64,
pub basis_points: f64,
}
impl TemporalFeatures {
/// Extract temporal features from timestamp
pub fn extract_features(timestamp: DateTime<Utc>) -> HashMap<String, f64> {
let mut features = HashMap::new();
// Time of day features
let hour = timestamp.hour() as f64;
let minute = timestamp.minute() as f64;
features.insert("hour".to_string(), hour);
features.insert("minute".to_string(), minute);
features.insert(
"hour_sin".to_string(),
(hour * 2.0 * std::f64::consts::PI / 24.0).sin(),
);
features.insert(
"hour_cos".to_string(),
(hour * 2.0 * std::f64::consts::PI / 24.0).cos(),
);
// Day of week features
let weekday = timestamp.weekday().num_days_from_monday() as f64;
features.insert("weekday".to_string(), weekday);
features.insert(
"is_weekend".to_string(),
if weekday >= 5.0 { 1.0 } else { 0.0 },
);
// Market session features (assuming US market hours)
let is_premarket = hour < 9.0 || (hour == 9.0 && minute < 30.0);
let is_regular_hours = (hour > 9.0 || (hour == 9.0 && minute >= 30.0)) && hour < 16.0;
let is_aftermarket = hour >= 16.0 && hour < 20.0;
features.insert(
"is_premarket".to_string(),
if is_premarket { 1.0 } else { 0.0 },
);
features.insert(
"is_regular_hours".to_string(),
if is_regular_hours { 1.0 } else { 0.0 },
);
features.insert(
"is_aftermarket".to_string(),
if is_aftermarket { 1.0 } else { 0.0 },
);
// Month and day features
let month = timestamp.month() as f64;
let day = timestamp.day() as f64;
features.insert("month".to_string(), month);
features.insert("day".to_string(), day);
features.insert(
"is_month_end".to_string(),
if day >= 28.0 { 1.0 } else { 0.0 },
);
features.insert(
"is_quarter_end".to_string(),
if month % 3.0 == 0.0 && day >= 28.0 {
1.0
} else {
0.0
},
);
features
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_technical_indicators_creation() {
let config = TechnicalIndicatorsConfig {
ma_periods: vec![10, 20],
rsi_periods: vec![14],
bollinger_periods: vec![20],
macd: crate::training_pipeline::MACDConfig {
fast_period: 12,
slow_period: 26,
signal_period: 9,
},
volume_indicators: true,
};
let indicators = TechnicalIndicators::new(config);
assert!(indicators.price_data.is_empty());
assert!(indicators.indicators.is_empty());
}
#[test]
fn test_temporal_features() {
let timestamp = Utc::now();
let features = TemporalFeatures::extract_features(timestamp);
assert!(features.contains_key("hour"));
assert!(features.contains_key("weekday"));
assert!(features.contains_key("is_regular_hours"));
}
#[test]
fn test_microstructure_analyzer() {
let config = MicrostructureConfig {
bid_ask_spread: true,
volume_imbalance: true,
price_impact: true,
kyle_lambda: false,
amihud_ratio: false,
roll_spread: false,
};
let analyzer = MicrostructureAnalyzer::new(config);
assert!(analyzer.order_books.is_empty());
assert!(analyzer.trade_data.is_empty());
}
}