Files
foxhunt/crates/data/src/features.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

2723 lines
89 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Feature Engineering for Financial ML Models
//!
//! Comprehensive feature engineering pipeline for HFT trading systems that transforms
//! raw market data into meaningful features for machine learning models. This module
//! provides a complete toolkit for quantitative finance feature extraction.
//!
//! ## Core Components
//!
//! ### Technical Indicators
//! - **Moving Averages**: Simple (SMA) and Exponential (EMA) moving averages
//! - **Momentum**: RSI, MACD, momentum oscillators
//! - **Volatility**: Bollinger Bands, Average True Range
//! - **Volume**: Volume-weighted indicators and flow analysis
//!
//! ### Market Microstructure Features
//! - **Spreads**: Bid-ask spread analysis and Roll spread estimation
//! - **Imbalances**: Volume and order book imbalances
//! - **Price Impact**: Kyle's lambda, Amihud illiquidity ratio
//! - **Liquidity**: Market depth and liquidity scoring
//!
//! ### TLOB (Time-Limited Order Book) Features
//! - **Order Flow**: Order flow imbalance and directional analysis
//! - **Book Dynamics**: Order book shape and dynamics
//! - **Execution Quality**: Slippage and execution cost analysis
//!
//! ### Temporal Features
//! - **Cyclical**: Hour of day, day of week patterns
//! - **Market Sessions**: Pre-market, regular hours, after-market
//! - **Calendar Effects**: Month-end, quarter-end, holiday effects
//!
//! ### Portfolio & Risk Features
//! - **Performance**: P&L tracking, Sharpe ratio, Sortino ratio
//! - **Risk Metrics**: VaR, Expected Shortfall, Maximum Drawdown
//! - **Exposure**: Beta, correlation analysis
//!
//! ## Architecture
//!
//! ```text
//! ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
//! │ Raw Market │────│ Feature │────│ ML Model │
//! │ Data │ │ Engineering │ │ Input │
//! └─────────────────┘ └─────────────────┘ └─────────────────┘
//! │ │ │
//! ▼ ▼ ▼
//! ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
//! │ Tick Data │ │ Technical │ │ Feature │
//! │ Order Books │────│ Indicators │────│ Vectors │
//! │ Trade Flow │ │ Microstructure│ │ (Normalized) │
//! └─────────────────┘ └─────────────────┘ └─────────────────┘
//! ```
//!
//! ## Performance Considerations
//!
//! - **Streaming Updates**: Incremental calculations for real-time processing
//! - **Memory Efficiency**: Rolling windows with automatic cleanup
//! - **SIMD Optimization**: Vectorized computations where possible
//! - **Caching**: Intelligent caching of intermediate calculations
//!
//! ## Usage Examples
//!
//! ```rust
//! use data::features::{
//! TechnicalIndicators, MicrostructureAnalyzer, TemporalFeatures,
//! FeatureVector, PricePoint
//! };
//! use config::data_config::TechnicalIndicatorsConfig;
//! use chrono::Utc;
//!
//! // Initialize technical indicators
//! let config = TechnicalIndicatorsConfig {
//! ma_periods: vec![10, 20, 50],
//! rsi_periods: vec![14],
//! bollinger_periods: vec![20],
//! // ... other config
//! };
//!
//! let mut indicators = TechnicalIndicators::new(config);
//!
//! // Update with new price data
//! let price_point = PricePoint {
//! timestamp: Utc::now(),
//! open: 100.0,
//! high: 101.5,
//! low: 99.5,
//! close: 101.0,
//! };
//!
//! indicators.update_price("AAPL", price_point);
//!
//! // Extract features
//! let tech_features = indicators.calculate_features("AAPL");
//! let temporal_features = TemporalFeatures::extract_features(Utc::now());
//!
//! // Combine into feature vector
//! let mut all_features = tech_features;
//! all_features.extend(temporal_features);
//!
//! let feature_vector = FeatureVector {
//! timestamp: Utc::now(),
//! symbol: "AAPL".to_string(),
//! features: all_features,
//! metadata: FeatureMetadata::default(),
//! };
//! ```
//!
//! ## Feature Categories
//!
//! Features are organized into categories for better model interpretation:
//!
//! - **Price**: OHLC-based features and price transformations
//! - **Volume**: Volume-based indicators and flow analysis
//! - **TechnicalIndicator**: Traditional TA indicators (RSI, MACD, etc.)
//! - **Microstructure**: Market microstructure and liquidity features
//! - **Temporal**: Time-based features and calendar effects
//! - **Regime**: Market regime and volatility state features
//! - **TLOB**: Time-Limited Order Book specific features
//! - **Portfolio**: Portfolio-level performance and risk metrics
//! - **Risk**: Risk management and exposure metrics
use chrono::{DateTime, Datelike, Timelike, Utc};
use config::data_config::{
DataMicrostructureConfig as MicrostructureConfig, DataTLOBConfig as TLOBConfig,
DataTechnicalIndicatorsConfig as TechnicalIndicatorsConfig,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, VecDeque};
/// Feature vector for ML model training and inference.
///
/// Represents a complete set of features extracted from market data at a specific
/// point in time. This is the primary data structure used to feed machine learning
/// models in the trading system.
///
/// # Structure
///
/// - **Timestamp**: When these features were calculated
/// - **Symbol**: The financial instrument these features apply to
/// - **Features**: Key-value map of feature names to numerical values
/// - **Metadata**: Additional information about feature quality and categories
///
/// # Feature Organization
///
/// Features are stored as a flat HashMap for efficient access, but can be
/// categorized using the metadata for model interpretation and debugging.
///
/// # Normalization
///
/// Features should be normalized before training ML models. The feature
/// engineering pipeline can apply various normalization techniques:
/// - Z-score normalization (mean=0, std=1)
/// - Min-max scaling (0-1 range)
/// - Robust scaling (using percentiles)
///
/// # Examples
///
/// ```rust
/// use data::features::{FeatureVector, FeatureMetadata, FeatureCategory};
/// use std::collections::HashMap;
/// use chrono::Utc;
///
/// let mut features = HashMap::new();
/// features.insert("sma_20".to_string(), 150.25);
/// features.insert("rsi_14".to_string(), 65.8);
/// features.insert("volume_ratio".to_string(), 1.2);
///
/// let feature_vector = FeatureVector {
/// timestamp: Utc::now(),
/// symbol: "AAPL".to_string(),
/// features,
/// metadata: FeatureMetadata::default(),
/// };
///
/// // Access specific features
/// let sma_value = feature_vector.features.get("sma_20").unwrap();
/// assert_eq!(*sma_value, 150.25);
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureVector {
/// Timestamp when these features were calculated
///
/// UTC timestamp indicating the exact time these features represent.
/// Critical for time-series analysis and ensuring proper temporal ordering.
pub timestamp: DateTime<Utc>,
/// Financial instrument symbol (ticker)
///
/// The security identifier (e.g., "AAPL", "SPY", "EURUSD") that these
/// features were calculated for. Used for symbol-specific model training.
pub symbol: String,
/// Feature name-value pairs
///
/// Map of feature names to their calculated numerical values.
/// Feature names should be descriptive and consistent across time
/// (e.g., "sma_20", "rsi_14", "bid_ask_spread_bps").
pub features: HashMap<String, f64>,
/// Feature metadata and quality information
///
/// Additional information about the features including descriptions,
/// categories, and data quality indicators.
pub metadata: FeatureMetadata,
}
/// Metadata and quality information for feature vectors.
///
/// Provides additional context about features including descriptions,
/// categorization, and data quality metrics. Essential for feature
/// interpretation, model debugging, and data quality monitoring.
///
/// # Quality Indicators
///
/// Quality indicators help identify potential data issues:
/// - **Completeness**: Percentage of non-null values (0.0-1.0)
/// - **Freshness**: How recent the underlying data is (0.0-1.0)
/// - **Reliability**: Confidence in data accuracy (0.0-1.0)
/// - **Stability**: Variance stability over time (0.0-1.0)
///
/// # Feature Categories
///
/// Categorization helps with:
/// - Feature selection and importance analysis
/// - Model interpretation and explainability
/// - Feature engineering pipeline organization
/// - Regulatory compliance and audit trails
///
/// # Examples
///
/// ```rust
/// use data::features::{FeatureMetadata, FeatureCategory};
/// use std::collections::HashMap;
///
/// let mut descriptions = HashMap::new();
/// descriptions.insert("sma_20".to_string(), "20-period Simple Moving Average".to_string());
/// descriptions.insert("rsi_14".to_string(), "14-period Relative Strength Index".to_string());
///
/// let mut categories = HashMap::new();
/// categories.insert("sma_20".to_string(), FeatureCategory::TechnicalIndicator);
/// categories.insert("rsi_14".to_string(), FeatureCategory::TechnicalIndicator);
///
/// let mut quality = HashMap::new();
/// quality.insert("sma_20".to_string(), 0.98); // 98% data completeness
/// quality.insert("rsi_14".to_string(), 0.95); // 95% data completeness
///
/// let metadata = FeatureMetadata {
/// feature_descriptions: descriptions,
/// feature_categories: categories,
/// quality_indicators: quality,
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureMetadata {
/// Human-readable descriptions of each feature
///
/// Maps feature names to their detailed descriptions explaining
/// what the feature represents and how it's calculated.
/// Essential for model documentation and interpretation.
pub feature_descriptions: HashMap<String, String>,
/// Categorical classification of features
///
/// Maps feature names to their category types for organization
/// and analysis. Helps with feature selection and model interpretation.
pub feature_categories: HashMap<String, FeatureCategory>,
/// Data quality metrics for each feature (0.0-1.0)
///
/// Maps feature names to quality scores indicating reliability,
/// completeness, and freshness of the underlying data.
/// Used for automated quality monitoring and alerts.
pub quality_indicators: HashMap<String, f64>,
/// Symbol these features belong to (for tests)
pub symbol: String,
/// Timestamp when metadata was created (for tests)
pub timestamp: DateTime<Utc>,
/// Total count of features (for tests)
pub feature_count: usize,
/// List of categories present (for tests)
pub categories: Vec<FeatureCategory>,
}
/// Categorical classification system for organizing features.
///
/// Provides a hierarchical way to organize features based on their
/// data source, calculation method, and business purpose. This
/// categorization is essential for:
///
/// - **Feature Selection**: Group-based importance analysis
/// - **Model Interpretation**: Understanding feature contributions by category
/// - **Data Lineage**: Tracking feature dependencies and data sources
/// - **Regulatory Compliance**: Documenting model inputs by data type
/// - **Performance Monitoring**: Category-specific quality metrics
///
/// # Category Descriptions
///
/// - **Price**: Features derived from OHLC price data
/// - **Volume**: Features based on trading volume and turnover
/// - **TechnicalIndicator**: Traditional technical analysis indicators
/// - **Microstructure**: Market microstructure and liquidity metrics
/// - **Temporal**: Time-based features and calendar effects
/// - **Regime**: Market regime and volatility state indicators
/// - **TLOB**: Time-Limited Order Book specific features
/// - **Portfolio**: Portfolio-level performance and allocation metrics
/// - **Risk**: Risk management and exposure indicators
///
/// # Usage in Feature Selection
///
/// ```rust
/// use data::features::{FeatureCategory, FeatureMetadata};
/// use std::collections::HashMap;
///
/// fn filter_technical_indicators(metadata: &FeatureMetadata) -> Vec<String> {
/// metadata.feature_categories
/// .iter()
/// .filter(|(_, category)| matches!(category, FeatureCategory::TechnicalIndicator))
/// .map(|(name, _)| name.clone())
/// .collect()
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum FeatureCategory {
/// Price-based features (OHLC, returns, price ratios)
///
/// Features derived directly from price data including:
/// - Open, High, Low, Close prices and their transformations
/// - Price returns and log returns
/// - Price ratios and relative price movements
/// - Gap analysis and price range metrics
Price,
/// Volume-based features (volume, turnover, VWAP)
///
/// Features calculated from trading volume data including:
/// - Raw volume and volume ratios
/// - Volume-weighted average price (VWAP)
/// - Volume rate of change
/// - Dollar volume and turnover metrics
Volume,
/// Traditional technical analysis indicators
///
/// Classic technical indicators including:
/// - Moving averages (SMA, EMA, WMA)
/// - Momentum indicators (RSI, MACD, Stochastic)
/// - Volatility indicators (Bollinger Bands, ATR)
/// - Trend indicators (ADX, Parabolic SAR)
TechnicalIndicator,
/// Market microstructure and liquidity features
///
/// Features related to market structure and liquidity including:
/// - Bid-ask spreads and spread components
/// - Order book imbalances and depth
/// - Price impact measures (Kyle's lambda, Amihud ratio)
/// - Trade classification and flow analysis
Microstructure,
/// Time-based and calendar features
///
/// Features derived from timestamps and calendar patterns:
/// - Hour of day, day of week effects
/// - Market session indicators (pre-market, regular hours)
/// - Calendar effects (month-end, quarter-end, holidays)
/// - Seasonal and cyclical patterns
Temporal,
/// Market regime and volatility state features
///
/// Features that capture market regime changes:
/// - Volatility regime indicators
/// - Trend vs. mean-reverting states
/// - Correlation regime changes
/// - Market stress indicators
Regime,
/// Time-Limited Order Book (TLOB) specific features
///
/// Features derived from order book dynamics:
/// - Order flow imbalances
/// - Book shape and slope analysis
/// - Order arrival and cancellation patterns
/// - Liquidity provision patterns
TLOB,
/// Portfolio-level performance and allocation features
///
/// Features calculated at the portfolio level:
/// - Portfolio returns and risk metrics
/// - Sector and style allocations
/// - Concentration and diversification measures
/// - Performance attribution factors
Portfolio,
/// Risk management and exposure features
///
/// Features related to risk measurement and control:
/// - Value at Risk (VaR) estimates
/// - Maximum drawdown metrics
/// - Exposure concentrations
/// - Correlation and beta measurements
Risk,
}
/// Technical indicators calculator for financial time series analysis.
///
/// Provides a comprehensive suite of technical analysis indicators commonly used
/// in quantitative trading and machine learning models. Supports streaming
/// calculations with automatic data management and efficient memory usage.
///
/// # Supported Indicators
///
/// - **Moving Averages**: Simple (SMA) and Exponential (EMA) moving averages
/// - **Momentum**: Relative Strength Index (RSI) with configurable periods
/// - **MACD**: Moving Average Convergence Divergence with signal line
/// - **Bollinger Bands**: Price bands with standard deviation channels
/// - **Volume Indicators**: Volume-based technical indicators
///
/// # Performance Features
///
/// - **Streaming Updates**: Incremental calculations for real-time processing
/// - **Memory Management**: Automatic cleanup of old data based on largest period
/// - **Multiple Timeframes**: Support for different periods simultaneously
/// - **State Persistence**: Maintains indicator state for continuous calculations
///
/// # Configuration
///
/// The calculator is configured via `TechnicalIndicatorsConfig` which specifies:
/// - Moving average periods (e.g., 10, 20, 50, 200)
/// - RSI calculation periods (e.g., 14, 21)
/// - Bollinger Bands periods and standard deviations
/// - MACD parameters (fast, slow, signal periods)
///
/// # Examples
///
/// ```rust
/// use data::features::{TechnicalIndicators, PricePoint};
/// use config::data_config::TechnicalIndicatorsConfig;
/// use chrono::Utc;
///
/// let config = TechnicalIndicatorsConfig {
/// ma_periods: vec![10, 20, 50],
/// rsi_periods: vec![14],
/// bollinger_periods: vec![20],
/// // ... other configuration
/// };
///
/// let mut indicators = TechnicalIndicators::new(config);
///
/// // Add price data
/// let price_point = PricePoint {
/// timestamp: Utc::now(),
/// open: 100.0,
/// high: 102.0,
/// low: 99.0,
/// close: 101.5,
/// };
///
/// indicators.update_price("AAPL", price_point);
///
/// // Calculate all features
/// let features = indicators.calculate_features("AAPL");
///
/// // Access specific indicators
/// if let Some(sma_20) = features.get("sma_20") {
/// println!("20-period SMA: {}", sma_20);
/// }
/// ```
///
/// # Data Requirements
///
/// Different indicators have different minimum data requirements:
/// - **SMA/EMA**: Requires at least 'period' data points
/// - **RSI**: Requires at least 'period + 1' data points
/// - **MACD**: Requires sufficient data for slow EMA calculation
/// - **Bollinger Bands**: Requires at least 'period' data points
///
/// Features will be omitted from output until sufficient data is available.
pub struct TechnicalIndicators {
pub config: TechnicalIndicatorsConfig,
pub price_data: BTreeMap<String, VecDeque<PricePoint>>,
pub volume_data: BTreeMap<String, VecDeque<VolumePoint>>,
pub indicators: BTreeMap<String, IndicatorState>,
}
/// OHLC price data point for technical indicator calculations.
///
/// Represents a single price observation with open, high, low, and close prices
/// along with a timestamp. This is the fundamental data unit for all technical
/// indicator calculations in the system.
///
/// # Data Integrity
///
/// Price points should satisfy basic integrity constraints:
/// - High >= max(Open, Close)
/// - Low <= min(Open, Close)
/// - All prices should be positive
/// - Timestamps should be in chronological order
///
/// # Usage in Indicators
///
/// Different indicators use different price components:
/// - **Moving Averages**: Typically use Close price
/// - **Bollinger Bands**: Use Close price for center line
/// - **ATR**: Uses High, Low, and previous Close
/// - **Price Channels**: Use High and Low prices
///
/// # Examples
///
/// ```rust
/// use data::features::PricePoint;
/// use chrono::Utc;
///
/// let price_point = PricePoint {
/// timestamp: Utc::now(),
/// open: 100.0,
/// high: 102.5, // Must be >= max(open, close)
/// low: 99.0, // Must be <= min(open, close)
/// close: 101.5,
/// };
///
/// // Validate price point integrity
/// assert!(price_point.high >= price_point.open.max(price_point.close));
/// assert!(price_point.low <= price_point.open.min(price_point.close));
/// ```
///
/// # Time Series Usage
///
/// When building time series for indicators:
/// - Maintain chronological order by timestamp
/// - Handle gaps in data appropriately
/// - Consider timezone consistency (use UTC)
/// - Validate price continuity and outliers
#[derive(Debug, Clone)]
pub struct PricePoint {
/// Timestamp of this price observation
///
/// UTC timestamp indicating when this price data represents.
/// Should be consistent with the timeframe being analyzed.
pub timestamp: DateTime<Utc>,
/// Opening price for the period
///
/// The first traded price during the time period.
/// For continuous markets, this is the first price after the previous close.
pub open: f64,
/// Highest price during the period
///
/// Must be greater than or equal to both open and close prices.
/// Used in volatility and range-based indicators.
pub high: f64,
/// Lowest price during the period
///
/// Must be less than or equal to both open and close prices.
/// Used in volatility and support/resistance analysis.
pub low: f64,
/// Closing price for the period
///
/// The last traded price during the time period.
/// Most commonly used price in technical indicators.
pub close: f64,
}
/// Volume data point for volume-based indicator calculations.
///
/// Represents trading volume information for a specific time period,
/// including both raw volume and volume-weighted average price (VWAP).
/// Essential for volume-based technical indicators and market analysis.
///
/// # Volume Metrics
///
/// - **Volume**: Total number of shares/contracts traded
/// - **VWAP**: Volume-weighted average price for the period
/// - **Dollar Volume**: Volume * Average Price (derived)
///
/// # Applications
///
/// Volume data is used in:
/// - Volume moving averages and oscillators
/// - On-Balance Volume (OBV) calculations
/// - Volume Rate of Change (VROC)
/// - Accumulation/Distribution indicators
/// - Money Flow Index (MFI)
///
/// # Data Quality
///
/// Volume data should be validated for:
/// - Non-negative volume values
/// - Reasonable VWAP relative to price range
/// - Consistency with price data timestamps
/// - Outlier detection for unusual volume spikes
///
/// # Examples
///
/// ```rust
/// use data::features::VolumePoint;
/// use chrono::Utc;
///
/// let volume_point = VolumePoint {
/// timestamp: Utc::now(),
/// volume: 1_000_000.0, // 1M shares traded
/// volume_weighted_price: 150.25, // VWAP for the period
/// };
///
/// // Calculate dollar volume
/// let dollar_volume = volume_point.volume * volume_point.volume_weighted_price;
/// assert_eq!(dollar_volume, 150_250_000.0); // $150.25M traded
/// ```
#[derive(Debug, Clone)]
pub struct VolumePoint {
/// Timestamp of this volume observation
///
/// UTC timestamp corresponding to the same period as the associated price data.
pub timestamp: DateTime<Utc>,
/// Total volume traded during the period
///
/// Number of shares, contracts, or units traded. Should be non-negative
/// and represent the total trading activity for the time period.
pub volume: f64,
/// Volume-weighted average price (VWAP) for the period
///
/// The average price weighted by trading volume, calculated as:
/// VWAP = Σ(Price × Volume) / Σ(Volume)
/// Provides a more representative average price than simple arithmetic mean.
pub volume_weighted_price: f64,
/// Buy-side volume for the period (for tests)
pub buy_volume: f64,
/// Sell-side volume for the period (for tests)
pub sell_volume: f64,
}
/// Internal state for maintaining technical indicator calculations.
///
/// Stores the current state of all technical indicators for a specific symbol,
/// enabling efficient incremental updates without recalculating from scratch.
/// This state persistence is crucial for real-time trading systems where
/// performance is critical.
///
/// # State Components
///
/// - **SMA**: Simple moving average values for different periods
/// - **EMA**: Exponential moving average values and their smoothing states
/// - **RSI**: Relative Strength Index values with gain/loss tracking
/// - **MACD**: Complete MACD state including EMA components
/// - **Bollinger**: Bollinger Bands state with statistics
///
/// # Memory Management
///
/// The state automatically manages memory by:
/// - Storing only current indicator values, not full history
/// - Using efficient data structures for O(1) updates
/// - Automatically cleaning up unused indicators
///
/// # Thread Safety
///
/// This structure is designed for single-threaded use per symbol.
/// For multi-threaded access, wrap in appropriate synchronization primitives.
///
/// # Examples
///
/// ```rust
/// use data::features::IndicatorState;
/// use std::collections::HashMap;
///
/// // State is typically managed internally by TechnicalIndicators
/// let mut state = IndicatorState {
/// sma: HashMap::new(),
/// ema: HashMap::new(),
/// rsi: HashMap::new(),
/// macd: Default::default(),
/// bollinger: HashMap::new(),
/// };
///
/// // Access specific indicator values
/// if let Some(sma_20) = state.sma.get(&20) {
/// println!("20-period SMA: {}", sma_20);
/// }
/// ```
#[derive(Debug, Clone)]
pub struct IndicatorState {
/// Simple Moving Average values by period
///
/// Maps period lengths to their current SMA values.
/// Updated with each new price point using rolling window calculation.
pub sma: HashMap<u32, f64>,
/// Exponential Moving Average values by period
///
/// Maps period lengths to their current EMA values.
/// Maintains smoothing state for efficient incremental updates.
pub ema: HashMap<u32, f64>,
/// Relative Strength Index values by period
///
/// Maps RSI periods to their current RSI values (0-100 scale).
/// Internally tracks average gains and losses for calculation.
pub rsi: HashMap<u32, f64>,
/// MACD (Moving Average Convergence Divergence) state
///
/// Complete MACD calculation state including MACD line, signal line,
/// histogram, and underlying EMA components.
pub macd: MACDState,
/// Bollinger Bands state by period
///
/// Maps periods to complete Bollinger Bands state including
/// upper/lower bands, bandwidth, and %B calculations.
pub bollinger: HashMap<u32, BollingerBandsState>,
}
/// MACD (Moving Average Convergence Divergence) indicator state.
///
/// Maintains the complete state for MACD calculation including the main MACD line,
/// signal line, histogram, and underlying exponential moving averages. MACD is
/// a trend-following momentum indicator that shows relationships between two
/// moving averages of prices.
///
/// # MACD Components
///
/// - **MACD Line**: Fast EMA - Slow EMA (typically 12-day - 26-day)
/// - **Signal Line**: EMA of MACD line (typically 9-day)
/// - **Histogram**: MACD Line - Signal Line
///
/// # Signal Interpretation
///
/// - **Bullish Signal**: MACD line crosses above signal line
/// - **Bearish Signal**: MACD line crosses below signal line
/// - **Momentum**: Histogram increasing = strengthening trend
/// - **Divergence**: MACD vs price divergence = potential reversal
///
/// # State Persistence
///
/// The state maintains the underlying EMA calculations to enable
/// efficient incremental updates without recalculating the entire history.
///
/// # Examples
///
/// ```rust
/// use data::features::MACDState;
///
/// let macd_state = MACDState {
/// macd_line: 1.25, // Fast EMA - Slow EMA
/// signal_line: 0.95, // EMA of MACD line
/// histogram: 0.30, // MACD line - Signal line
/// fast_ema: 150.25, // Current fast EMA value
/// slow_ema: 149.00, // Current slow EMA value
/// signal_ema: 0.95, // Signal line EMA value
/// };
///
/// // Check for bullish crossover
/// let is_bullish = macd_state.macd_line > macd_state.signal_line &&
/// macd_state.histogram > 0.0;
/// ```
#[derive(Debug, Clone)]
pub struct MACDState {
/// MACD line value (Fast EMA - Slow EMA)
///
/// The main MACD indicator calculated as the difference between
/// fast and slow exponential moving averages. Positive values
/// indicate upward momentum, negative values indicate downward momentum.
pub macd_line: f64,
/// Signal line value (EMA of MACD line)
///
/// The signal line is an exponential moving average of the MACD line,
/// used to generate buy/sell signals through crossovers with the MACD line.
pub signal_line: f64,
/// MACD histogram (MACD line - Signal line)
///
/// The difference between MACD line and signal line, displayed as
/// a histogram. Shows the convergence and divergence of the two lines.
pub histogram: f64,
/// Last update timestamp (for tests)
pub last_update: DateTime<Utc>,
/// Current fast EMA value
///
/// The faster exponential moving average component (typically 12-period).
/// Maintained for efficient incremental calculation updates.
pub fast_ema: f64,
/// Current slow EMA value
///
/// The slower exponential moving average component (typically 26-period).
/// Maintained for efficient incremental calculation updates.
pub slow_ema: f64,
/// Current signal line EMA value
///
/// The EMA used for the signal line calculation (typically 9-period).
/// Maintained for efficient incremental calculation updates.
pub signal_ema: f64,
}
/// Bollinger Bands indicator state and calculations.
///
/// Bollinger Bands consist of a middle line (moving average) and two bands
/// (upper and lower) that are standard deviations away from the middle line.
/// They are used to identify overbought/oversold conditions and volatility.
///
/// # Band Components
///
/// - **Upper Band**: Middle line + (2 × Standard Deviation)
/// - **Middle Band**: Simple Moving Average (typically 20-period)
/// - **Lower Band**: Middle line - (2 × Standard Deviation)
///
/// # Derived Metrics
///
/// - **Bandwidth**: (Upper Band - Lower Band) / Middle Band
/// - **%B**: (Price - Lower Band) / (Upper Band - Lower Band)
///
/// # Trading Signals
///
/// - **Bollinger Squeeze**: Low bandwidth indicates low volatility
/// - **Band Walking**: Price staying near upper/lower band indicates strong trend
/// - **Mean Reversion**: Price touching bands often reverses toward middle
/// - **Breakouts**: Price breaking outside bands can signal trend continuation
///
/// # Examples
///
/// ```rust
/// use data::features::BollingerBandsState;
///
/// let bb_state = BollingerBandsState {
/// upper_band: 152.50,
/// middle_band: 150.00, // 20-period SMA
/// lower_band: 147.50,
/// bandwidth: 0.033, // 3.3% bandwidth
/// percent_b: 0.75, // Price at 75% of band range
/// };
///
/// // Check for squeeze condition (low volatility)
/// let is_squeeze = bb_state.bandwidth < 0.05; // Less than 5%
///
/// // Check overbought/oversold conditions
/// let is_overbought = bb_state.percent_b > 0.8;
/// let is_oversold = bb_state.percent_b < 0.2;
/// ```
#[derive(Debug, Clone)]
pub struct BollingerBandsState {
/// Upper Bollinger Band (Middle + 2 × Std Dev)
///
/// The upper boundary of the Bollinger Bands, typically set at
/// 2 standard deviations above the middle line. Prices touching
/// or exceeding this level may indicate overbought conditions.
pub upper_band: f64,
/// Middle Bollinger Band (Simple Moving Average)
///
/// The center line of Bollinger Bands, typically a 20-period
/// simple moving average. Acts as dynamic support/resistance.
pub middle_band: f64,
/// Lower Bollinger Band (Middle - 2 × Std Dev)
///
/// The lower boundary of the Bollinger Bands, typically set at
/// 2 standard deviations below the middle line. Prices touching
/// or falling below this level may indicate oversold conditions.
pub lower_band: f64,
/// Bandwidth ratio ((Upper - Lower) / Middle)
///
/// Measures the width of the bands relative to the middle band.
/// Low bandwidth indicates low volatility ("squeeze"),
/// high bandwidth indicates high volatility.
pub bandwidth: f64,
/// %B indicator ((Price - Lower) / (Upper - Lower))
///
/// Shows where the price is relative to the bands:
/// - 1.0 = At upper band
/// - 0.5 = At middle band
/// - 0.0 = At lower band
/// Values outside 0-1 indicate price outside the bands.
pub percent_b: f64,
/// Last update timestamp (for tests)
pub last_update: DateTime<Utc>,
}
/// Market microstructure analyzer for liquidity and trading cost analysis.
///
/// Analyzes the detailed structure of financial markets including bid-ask spreads,
/// order book dynamics, price impact, and liquidity measures. Essential for
/// high-frequency trading strategies and execution cost analysis.
///
/// # Microstructure Features
///
/// - **Spreads**: Bid-ask spread, effective spread, Roll spread
/// - **Imbalances**: Volume imbalance, order book imbalance
/// - **Price Impact**: Kyle's lambda, Amihud illiquidity ratio
/// - **Liquidity**: Market depth, liquidity scores
/// - **Trade Classification**: Buy/sell classification, trade direction
///
/// # Data Sources
///
/// The analyzer processes multiple data streams:
/// - **Order Books**: Level 1 and Level 2 market data
/// - **Trades**: Time and sales data with trade classification
/// - **Quotes**: Bid/ask quotes with sizes
///
/// # Applications
///
/// - **Execution Analysis**: Measuring transaction costs and market impact
/// - **Market Making**: Optimal spread setting and inventory management
/// - **Regime Detection**: Identifying changes in market liquidity conditions
/// - **Risk Management**: Liquidity risk assessment and monitoring
///
/// # Performance Considerations
///
/// - **Real-time Processing**: Designed for sub-millisecond latency
/// - **Memory Efficiency**: Rolling windows for historical calculations
/// - **Configurable Features**: Enable only needed calculations for performance
///
/// # Examples
///
/// ```rust
/// use data::features::{MicrostructureAnalyzer, QuoteData, TradeData};
/// use config::data_config::MicrostructureConfig;
/// use chrono::Utc;
///
/// let config = MicrostructureConfig {
/// bid_ask_spread: true,
/// volume_imbalance: true,
/// price_impact: true,
/// kyle_lambda: false, // Computationally expensive
/// amihud_ratio: true,
/// // ... other settings
/// };
///
/// let mut analyzer = MicrostructureAnalyzer::new(config);
///
/// // Update with market data
/// let quote = QuoteData {
/// timestamp: Utc::now(),
/// bid: 149.95,
/// ask: 150.05,
/// bid_size: 1000.0,
/// ask_size: 800.0,
/// };
/// analyzer.update_quote("AAPL", quote);
///
/// // Calculate microstructure features
/// let features = analyzer.calculate_features("AAPL");
///
/// if let Some(spread_bps) = features.get("bid_ask_spread_bps") {
/// println!("Bid-ask spread: {} bps", spread_bps);
/// }
/// ```
pub struct MicrostructureAnalyzer {
pub config: MicrostructureConfig,
pub order_books: HashMap<String, OrderBookState>,
pub trade_data: BTreeMap<String, VecDeque<TradeData>>,
pub quote_data: BTreeMap<String, VecDeque<QuoteData>>,
}
/// Order book state snapshot for microstructure analysis.
///
/// Represents a point-in-time view of the order book including bids, asks,
/// and derived metrics like spread and imbalance. Used for calculating
/// market microstructure features and liquidity analysis.
///
/// # Order Book Metrics
///
/// - **Mid Price**: (Best Bid + Best Ask) / 2
/// - **Spread**: Best Ask - Best Bid (absolute)
/// - **Imbalance**: (Bid Size - Ask Size) / (Bid Size + Ask Size)
/// - **Depth**: Total volume available at best levels
///
/// # Data Quality
///
/// Order book states should be validated for:
/// - Best bid < Best ask (no crossed market)
/// - Positive sizes for all price levels
/// - Proper price level ordering (ascending asks, descending bids)
/// - Timestamp consistency with market data
///
/// # Applications
///
/// - **Spread Analysis**: Transaction cost estimation
/// - **Liquidity Assessment**: Available depth and market impact
/// - **Imbalance Signals**: Short-term price direction prediction
/// - **Market Making**: Optimal quote placement strategies
///
/// # Examples
///
/// ```rust
/// use data::features::OrderBookState;
/// use common::PriceLevel;
/// use chrono::Utc;
///
/// let order_book = OrderBookState {
/// timestamp: Utc::now(),
/// bids: vec![
/// PriceLevel { price: 149.95.into(), size: 1000.into() },
/// PriceLevel { price: 149.90.into(), size: 1500.into() },
/// ],
/// asks: vec![
/// PriceLevel { price: 150.05.into(), size: 800.into() },
/// PriceLevel { price: 150.10.into(), size: 1200.into() },
/// ],
/// mid_price: 150.00,
/// spread: 0.10,
/// imbalance: 0.111, // (1000-800)/(1000+800)
/// depth: 1800.0, // Total at best levels
/// };
/// ```
#[derive(Debug, Clone)]
pub struct OrderBookState {
pub timestamp: DateTime<Utc>,
pub best_bid: f64,
pub best_ask: f64,
pub bid_size: f64,
pub ask_size: f64,
pub spread: f64,
pub mid_price: f64,
}
// PriceLevel moved to canonical source in common::types
// Note: Original used f64 types - code may need conversion to Decimal
/// Individual trade data for microstructure analysis.
///
/// Represents a single trade execution with price, size, direction, and timing
/// information. Used for calculating trade-based microstructure features like
/// price impact, trade classification, and flow analysis.
///
/// # Trade Classification
///
/// Trade direction is classified using various methods:
/// - **Tick Rule**: Compare trade price to previous trade price
/// - **Quote Rule**: Compare trade price to midpoint of bid-ask
/// - **LR Algorithm**: Lee-Ready algorithm combining tick and quote rules
/// - **Bulk Volume Classification**: Statistical methods for block trades
///
/// # Applications
///
/// - **Order Flow Analysis**: Measuring buy vs sell pressure
/// - **Price Impact**: Analyzing effect of trades on subsequent prices
/// - **Trade Size Analysis**: Volume distribution and block trade detection
/// - **Execution Quality**: Measuring implementation shortfall and slippage
///
/// # Data Quality
///
/// Trade data should be validated for:
/// - Positive trade sizes
/// - Reasonable prices within market ranges
/// - Proper timestamp ordering
/// - Trade direction consistency with price movements
///
/// # Examples
///
/// ```rust
/// use data::features::{TradeData, TradeDirection};
/// use chrono::Utc;
///
/// let trade = TradeData {
/// timestamp: Utc::now(),
/// price: 150.05,
/// size: 1000.0,
/// direction: TradeDirection::Buy, // Classified as buyer-initiated
/// };
///
/// // Calculate trade value
/// let trade_value = trade.price * trade.size; // $150,050
///
/// // Check for block trade
/// let is_block_trade = trade.size > 10_000.0;
/// ```
#[derive(Debug, Clone)]
pub struct TradeData {
pub timestamp: DateTime<Utc>,
pub price: f64,
pub size: f64,
pub direction: TradeDirection,
pub conditions: Vec<String>,
}
/// Quote data (bid/ask prices and sizes) for microstructure analysis.
///
/// Represents the best bid and offer (BBO) at a specific point in time,
/// including both prices and sizes. Essential for calculating spreads,
/// imbalances, and other microstructure features.
///
/// # Quote Components
///
/// - **Bid**: Highest price buyers are willing to pay
/// - **Ask**: Lowest price sellers are willing to accept
/// - **Bid Size**: Quantity available at the bid price
/// - **Ask Size**: Quantity available at the ask price
///
/// # Derived Metrics
///
/// From quote data, we can calculate:
/// - **Spread**: Ask - Bid
/// - **Mid Price**: (Ask + Bid) / 2
/// - **Imbalance**: (Bid Size - Ask Size) / (Bid Size + Ask Size)
/// - **Depth**: Bid Size + Ask Size
///
/// # Data Integrity
///
/// Quote data should satisfy:
/// - Ask price >= Bid price (no crossed market in normal conditions)
/// - Positive sizes for both bid and ask
/// - Reasonable spread relative to price level
/// - Timestamps in chronological order
///
/// # Examples
///
/// ```rust
/// use data::features::QuoteData;
/// use chrono::Utc;
///
/// let quote = QuoteData {
/// timestamp: Utc::now(),
/// bid: 149.95,
/// ask: 150.05,
/// bid_size: 1000.0,
/// ask_size: 800.0,
/// };
///
/// // Calculate derived metrics
/// let spread = quote.ask - quote.bid; // 0.10
/// let mid_price = (quote.ask + quote.bid) / 2.0; // 150.00
/// let imbalance = (quote.bid_size - quote.ask_size) / // 0.111
/// (quote.bid_size + quote.ask_size);
/// ```
#[derive(Debug, Clone)]
pub struct QuoteData {
pub timestamp: DateTime<Utc>,
pub bid_price: f64,
pub ask_price: f64,
pub bid_size: f64,
pub ask_size: f64,
pub exchange: String,
}
/// Classification of trade direction for order flow analysis.
///
/// Determines whether a trade was initiated by a buyer (market buy order)
/// or seller (market sell order), which is crucial for understanding
/// order flow and price pressure in the market.
///
/// # Classification Methods
///
/// - **Tick Rule**: Compare to previous trade price
/// - Uptick (higher price) = Buy
/// - Downtick (lower price) = Sell
/// - Zero tick = Use previous classification
///
/// - **Quote Rule**: Compare to bid-ask midpoint
/// - Above midpoint = Buy
/// - Below midpoint = Sell
/// - At midpoint = Unknown
///
/// - **Lee-Ready Algorithm**: Combination of tick and quote rules
/// - Use quote rule first
/// - If at midpoint, use tick rule
///
/// # Applications
///
/// - **Order Flow Imbalance**: Net buying vs selling pressure
/// - **Price Impact**: Effect of buyer vs seller initiated trades
/// - **Market Microstructure**: Understanding trade dynamics
/// - **Execution Analysis**: Assessing market impact of strategies
///
/// # Examples
///
/// ```rust
/// use data::features::TradeDirection;
///
/// // Classify trades based on price movement
/// fn classify_by_tick_rule(current_price: f64, previous_price: f64) -> TradeDirection {
/// if current_price > previous_price {
/// TradeDirection::Buy
/// } else if current_price < previous_price {
/// TradeDirection::Sell
/// } else {
/// TradeDirection::Unknown
/// }
/// }
/// ```
#[derive(Debug, Clone)]
pub enum TradeDirection {
/// Buyer-initiated trade (market buy order)
///
/// Trade was initiated by an aggressive buyer using a market order
/// to purchase at the best available ask price. Indicates positive
/// price pressure and buying interest.
Buy,
/// Seller-initiated trade (market sell order)
///
/// Trade was initiated by an aggressive seller using a market order
/// to sell at the best available bid price. Indicates negative
/// price pressure and selling interest.
Sell,
/// Unknown or indeterminate trade direction
///
/// Trade direction could not be determined reliably, either due to:
/// - Trade at exact midpoint with no prior price reference
/// - Insufficient data for classification algorithms
/// - Special trade conditions (e.g., block trades, opening auctions)
Unknown,
}
/// TLOB (Time-Limited Order Book) analyzer
pub struct TLOBAnalyzer {
pub config: TLOBConfig,
pub snapshots: BTreeMap<String, VecDeque<TLOBSnapshot>>,
pub order_flow: BTreeMap<String, VecDeque<OrderFlowEvent>>,
}
/// TLOB snapshot for analysis
#[derive(Debug, Clone)]
pub struct TLOBSnapshot {
pub timestamp: DateTime<Utc>,
pub bid_levels: Vec<(f64, f64)>,
pub ask_levels: Vec<(f64, f64)>,
pub mid_price: f64,
pub weighted_mid: f64,
pub imbalance: 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: String,
}
/// Order flow event types
#[derive(Debug, Clone)]
pub enum OrderFlowEventType {
NewOrder,
OrderCancel,
OrderModify,
Trade,
MarketDataUpdate,
}
/// Temporal feature extractor for time-based market patterns.
///
/// Extracts features related to time patterns, market sessions, and calendar
/// effects that can be predictive in financial markets. These features help
/// capture cyclical patterns and regime changes based on time of day,
/// day of week, and other temporal factors.
///
/// # Extracted Features
///
/// ### Time of Day
/// - Hour and minute components
/// - Cyclical encoding using sine/cosine transforms
/// - Market session indicators (pre-market, regular hours, after-hours)
///
/// ### Calendar Effects
/// - Day of week (Monday effect, Friday effect)
/// - Weekend indicators
/// - Month and day of month
/// - Month-end and quarter-end effects
///
/// ### Market Sessions (US Market)
/// - **Pre-market**: 4:00 AM - 9:30 AM ET
/// - **Regular Hours**: 9:30 AM - 4:00 PM ET
/// - **After-hours**: 4:00 PM - 8:00 PM ET
///
/// # Cyclical Encoding
///
/// Time components are encoded using sine and cosine functions to capture
/// cyclical nature and ensure smooth transitions (e.g., 23:59 to 00:00):
///
/// ```text
/// hour_sin = sin(hour * 2π / 24)
/// hour_cos = cos(hour * 2π / 24)
/// ```
///
/// # Applications
///
/// - **Intraday Patterns**: Volume and volatility changes throughout the day
/// - **Weekly Patterns**: Monday gaps, Friday positioning effects
/// - **Calendar Effects**: Month-end rebalancing, quarterly window dressing
/// - **Regime Detection**: Identifying different market regimes by time
///
/// # Examples
///
/// ```rust
/// use data::features::TemporalFeatures;
/// use chrono::{Utc, Timelike, Datelike};
///
/// let timestamp = Utc::now();
/// let features = TemporalFeatures::extract_features(timestamp);
///
/// // Check for specific time-based conditions
/// let is_market_open = features.get("is_regular_hours").unwrap_or(&0.0) > 0.5;
/// let is_friday = features.get("weekday").unwrap_or(&0.0) == &4.0;
/// let is_month_end = features.get("is_month_end").unwrap_or(&0.0) > 0.5;
///
/// if is_market_open && is_friday {
/// println!("Friday regular trading hours");
/// }
/// ```
///
/// # Implementation Note
///
/// This is a utility struct with static methods. All functionality is
/// provided through the `extract_features` associated function.
pub struct TemporalFeatures;
/// Configuration for regime detector
#[derive(Debug, Clone)]
pub struct RegimeDetectorConfig {
pub lookback_periods: usize,
pub volatility_threshold: f64,
pub trend_threshold: f64,
pub correlation_threshold: f64,
pub rebalance_frequency: usize,
}
/// Regime detection analyzer
pub struct RegimeDetector {
pub config: RegimeDetectorConfig,
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>>,
}
/// Configuration for portfolio analyzer
#[derive(Debug, Clone)]
pub struct PortfolioAnalyzerConfig {
pub risk_free_rate: f64,
pub target_return: f64,
pub rebalance_threshold: f64,
pub max_position_size: f64,
pub diversification_target: usize,
}
/// Portfolio performance analyzer
pub struct PortfolioAnalyzer {
pub config: PortfolioAnalyzerConfig,
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 entry_price: f64,
pub current_price: f64,
pub entry_time: DateTime<Utc>,
pub last_update: DateTime<Utc>,
}
/// P&L tracking point
#[derive(Debug, Clone)]
pub struct PnLPoint {
pub timestamp: DateTime<Utc>,
pub realized_pnl: f64,
pub unrealized_pnl: f64,
pub total_pnl: f64,
pub cumulative_pnl: f64,
}
/// Risk metrics
#[derive(Debug, Clone)]
pub struct RiskMetrics {
pub var_95: f64,
pub var_99: f64,
pub expected_shortfall: f64,
pub sharpe_ratio: f64,
pub sortino_ratio: f64,
pub max_drawdown: f64,
pub volatility: 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 as u32)) {
features.insert(format!("sma_{}", period), sma);
}
}
// Exponential Moving Averages
for &period in &self.config.ma_periods {
if let Some(&ema) = indicators.ema.get(&(period as u32)) {
features.insert(format!("ema_{}", period), ema);
}
}
// RSI
for &period in &self.config.rsi_periods {
if let Some(&rsi) = indicators.rsi.get(&(period as u32)) {
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 as u32)) {
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,
last_update: Utc::now(),
},
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 as u32) {
indicator_state.sma.insert(period as u32, sma);
}
}
// Update EMA
for &period in &ma_periods {
if let Some(ema) = Self::calculate_ema_static(
&price_data,
period as u32,
current_ema_values.get(&(period as u32)).copied(),
) {
indicator_state.ema.insert(period as u32, ema);
}
}
// Update RSI
for &period in &rsi_periods {
if let Some(rsi) = Self::calculate_rsi_static(&price_data, period as u32) {
indicator_state.rsi.insert(period as u32, 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 as u32) {
indicator_state.bollinger.insert(period as u32, 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()?.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 = match data.back() {
Some(price_point) => price_point.close,
None => return prev_state.clone(), // Return previous state if data is unexpectedly empty
};
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,
last_update: Utc::now(),
}
}
/// 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()?.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,
last_update: Utc::now(),
})
}
// 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()?.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;
// SAFETY: Loop bound ensures idx and prev_idx are valid indices.
// We check data.len() >= period + 1 above, so:
// - idx = data.len() - 1 - i where i < period, so idx >= 0
// - prev_idx = data.len() - 2 - i where i < period, so prev_idx >= 0
#[allow(clippy::indexing_slicing)]
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 = match data.back() {
Some(point) => point.close,
None => return prev_state.clone(),
};
// 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,
last_update: Utc::now(),
}
}
/// 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()?.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,
last_update: Utc::now(),
})
}
}
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.bid_ask_spread);
features.insert("relative_spread".to_string(), spread.relative_spread);
features.insert("effective_spread".to_string(), spread.effective_spread);
}
}
// 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 (using bid_ask_spread field)
if self.config.bid_ask_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 bid_ask_spread = latest_quote.ask_price - latest_quote.bid_price;
let mid_price = (latest_quote.ask_price + latest_quote.bid_price) / 2.0;
let relative_spread = bid_ask_spread / mid_price;
Some(SpreadMetrics {
bid_ask_spread,
relative_spread,
effective_spread: bid_ask_spread * 0.5,
realized_spread: bid_ask_spread * 0.3,
price_impact: bid_ask_spread * 0.2,
})
}
/// 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).
///
/// Kyle's lambda is the regression coefficient of price change on signed order flow:
/// lambda = cov(delta_price, signed_volume) / var(signed_volume)
///
/// Uses a rolling window of recent trades. Signed volume is determined by the tick rule:
/// sign = sign(price_change) applied to volume.
fn calculate_kyle_lambda(&self, symbol: &str) -> Option<f64> {
let trade_data = self.trade_data.get(symbol)?;
let quote_data = self.quote_data.get(symbol)?;
if trade_data.len() < 20 || quote_data.len() < 10 {
return None;
}
// Use the most recent 100 trades (or all available if fewer)
let window_size = 100.min(trade_data.len());
let recent_trades: Vec<&TradeData> = trade_data.iter().rev().take(window_size).collect();
// Need at least 20 consecutive trades for meaningful regression
if recent_trades.len() < 20 {
return None;
}
// Compute price changes and signed volumes using tick rule
let mut price_changes = Vec::with_capacity(recent_trades.len() - 1);
let mut signed_volumes = Vec::with_capacity(recent_trades.len() - 1);
for window in recent_trades.windows(2) {
let current = window[0];
let previous = window[1];
let delta_price = current.price - previous.price;
// Tick rule: sign volume by the direction of the price change
let sign = if delta_price > 0.0 {
1.0
} else if delta_price < 0.0 {
-1.0
} else {
// No price change: use the trade's classified direction if available
match current.direction {
TradeDirection::Buy => 1.0,
TradeDirection::Sell => -1.0,
TradeDirection::Unknown => 0.0,
}
};
let signed_vol = current.size * sign;
price_changes.push(delta_price);
signed_volumes.push(signed_vol);
}
let n = price_changes.len() as f64;
if n < 2.0 {
return None;
}
// Compute means
let mean_dp = price_changes.iter().sum::<f64>() / n;
let mean_sv = signed_volumes.iter().sum::<f64>() / n;
// Compute covariance and variance
let mut cov = 0.0;
let mut var_sv = 0.0;
for i in 0..price_changes.len() {
let dp_diff = price_changes[i] - mean_dp;
let sv_diff = signed_volumes[i] - mean_sv;
cov += dp_diff * sv_diff;
var_sv += sv_diff * sv_diff;
}
cov /= n;
var_sv /= n;
// Avoid division by zero (no variance in signed volume)
if var_sv.abs() < 1e-18 {
return None;
}
let lambda = cov / var_sv;
// Kyle's lambda should be non-negative (higher order flow -> higher price impact)
// A negative value indicates the model is not well-specified for this data window
if lambda < 0.0 {
None
} else {
Some(lambda)
}
}
/// 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())
}
}
/// Comprehensive bid-ask spread metrics for transaction cost analysis.
///
/// Provides multiple representations of the bid-ask spread to support
/// different analysis requirements and to normalize spreads across
/// different price levels and market conditions.
///
/// # Spread Representations
///
/// - **Absolute**: Raw price difference (Ask - Bid)
/// - **Percentage**: Relative to mid-price ((Ask - Bid) / Mid-price)
/// - **Basis Points**: Percentage × 10,000 for easier interpretation
///
/// # Applications
///
/// - **Transaction Cost Analysis**: Estimating cost of immediate execution
/// - **Liquidity Assessment**: Lower spreads indicate higher liquidity
/// - **Market Comparison**: Normalized spreads enable cross-asset comparison
/// - **Regime Detection**: Spread changes indicate market stress or calm
///
/// # Interpretation Guidelines
///
/// - **Tight Spreads** (< 5 bps): Highly liquid, low transaction costs
/// - **Normal Spreads** (5-20 bps): Standard liquidity for most assets
/// - **Wide Spreads** (> 20 bps): Lower liquidity, higher transaction costs
/// - **Very Wide Spreads** (> 100 bps): Illiquid or stressed market conditions
///
/// # Examples
///
/// ```rust
/// use data::features::SpreadMetrics;
///
/// let spread = SpreadMetrics {
/// absolute: 0.05, // 5 cent spread
/// percentage: 0.0003, // 0.03% of mid-price
/// basis_points: 3.0, // 3 basis points
/// };
///
/// // Classify liquidity based on spread
/// let liquidity_tier = match spread.basis_points {
/// bp if bp < 5.0 => "Highly Liquid",
/// bp if bp < 20.0 => "Liquid",
/// bp if bp < 50.0 => "Moderately Liquid",
/// _ => "Illiquid",
/// };
/// ```
#[derive(Debug, Clone)]
pub struct SpreadMetrics {
pub bid_ask_spread: f64,
pub relative_spread: f64,
pub effective_spread: f64,
pub realized_spread: f64,
pub price_impact: f64,
}
impl TemporalFeatures {
/// Extract temporal features from timestamp
pub fn extract_features(timestamp: DateTime<Utc>) -> HashMap<String, f64> {
let mut features = HashMap::new();
// Convert UTC to EST (UTC-5) for market hour calculations
// Note: This doesn't account for daylight saving time, assumes EST year-round
use chrono::FixedOffset;
// SAFETY: 5*3600 = 18000 seconds is always a valid UTC offset
#[allow(clippy::unwrap_used)]
let est_offset = FixedOffset::west_opt(5 * 3600).unwrap();
let est_time = timestamp.with_timezone(&est_offset);
// Time of day features (using EST for market hours)
let hour = est_time.hour() as f64;
let minute = est_time.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
}
}
impl TLOBAnalyzer {
pub fn new(config: TLOBConfig) -> Self {
Self {
config,
snapshots: BTreeMap::new(),
order_flow: BTreeMap::new(),
}
}
}
impl RegimeDetector {
pub fn new(config: RegimeDetectorConfig) -> Self {
Self {
config,
volatility_history: BTreeMap::new(),
volume_history: BTreeMap::new(),
price_history: BTreeMap::new(),
correlation_matrix: HashMap::new(),
}
}
}
impl PortfolioAnalyzer {
pub fn new(config: PortfolioAnalyzerConfig) -> Self {
Self {
config,
positions: HashMap::new(),
pnl_history: VecDeque::new(),
risk_metrics: RiskMetrics {
var_95: 0.0,
var_99: 0.0,
expected_shortfall: 0.0,
sharpe_ratio: 0.0,
sortino_ratio: 0.0,
max_drawdown: 0.0,
volatility: 0.0,
},
}
}
}
#[cfg(test)]
#[allow(clippy::get_unwrap)]
mod tests {
use super::*;
#[test]
fn test_technical_indicators_creation() {
let config = TechnicalIndicatorsConfig {
enable_moving_averages: true,
enable_momentum: true,
enable_volatility: true,
window_sizes: vec![10, 20],
ma_periods: vec![10, 20],
rsi_periods: vec![14],
bollinger_periods: vec![20],
macd: config::data_config::DataMACDConfig {
fast_period: 12,
slow_period: 26,
signal_period: 9,
enabled: 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 {
enable_bid_ask_spread: true,
enable_order_flow: true,
tick_size: 0.01,
lot_size: 100.0,
bid_ask_spread: true,
volume_imbalance: true,
price_impact: true,
kyle_lambda: false,
amihud_ratio: false,
};
let analyzer = MicrostructureAnalyzer::new(config);
assert!(analyzer.order_books.is_empty());
assert!(analyzer.trade_data.is_empty());
}
#[test]
fn test_feature_vector_creation() {
let mut features = HashMap::new();
features.insert("price".to_string(), 100.0);
features.insert("volume".to_string(), 1000.0);
let mut feature_categories = HashMap::new();
feature_categories.insert("price".to_string(), FeatureCategory::Price);
feature_categories.insert("volume".to_string(), FeatureCategory::Volume);
let metadata = FeatureMetadata {
symbol: "AAPL".to_string(),
timestamp: Utc::now(),
feature_count: 2,
categories: vec![FeatureCategory::Price, FeatureCategory::Volume],
feature_descriptions: HashMap::new(),
feature_categories,
quality_indicators: HashMap::new(),
};
let vector = FeatureVector {
timestamp: Utc::now(),
symbol: "AAPL".to_string(),
features,
metadata: metadata.clone(),
};
assert_eq!(vector.features.len(), 2);
assert_eq!(vector.metadata.feature_count, 2);
assert_eq!(vector.metadata.symbol, "AAPL");
}
#[test]
fn test_technical_indicators_update() {
let config = TechnicalIndicatorsConfig {
enable_moving_averages: true,
enable_momentum: true,
enable_volatility: true,
window_sizes: vec![5],
ma_periods: vec![5],
rsi_periods: vec![14],
bollinger_periods: vec![20],
macd: config::data_config::DataMACDConfig {
fast_period: 12,
slow_period: 26,
signal_period: 9,
enabled: true,
},
};
let mut indicators = TechnicalIndicators::new(config);
// Add some price points
for i in 0..10 {
let price = PricePoint {
timestamp: Utc::now(),
open: 100.0 + i as f64,
high: 102.0 + i as f64,
low: 99.0 + i as f64,
close: 101.0 + i as f64,
};
indicators.update_price("AAPL", price);
}
// price_data is a HashMap<String, VecDeque<PricePoint>>
// We expect 1 symbol ("AAPL")
// Data is trimmed to max_period (5 in this test's config)
assert_eq!(indicators.price_data.len(), 1);
assert_eq!(indicators.price_data.get("AAPL").unwrap().len(), 5);
}
#[test]
fn test_volume_point_validation() {
let volume = VolumePoint {
timestamp: Utc::now(),
volume: 1000.0,
volume_weighted_price: 100.5,
buy_volume: 600.0,
sell_volume: 400.0,
};
assert_eq!(volume.volume, 1000.0);
assert_eq!(volume.buy_volume, 600.0);
assert!(volume.buy_volume + volume.sell_volume == 1000.0);
}
#[test]
fn test_macd_state() {
let state = MACDState {
macd_line: 2.5,
signal_line: 2.0,
histogram: 0.5,
fast_ema: 102.0,
slow_ema: 99.5,
signal_ema: 2.0,
last_update: Utc::now(),
};
assert_eq!(state.macd_line - state.signal_line, state.histogram);
}
#[test]
fn test_bollinger_bands_state() {
let state = BollingerBandsState {
upper_band: 105.0,
middle_band: 100.0,
lower_band: 95.0,
bandwidth: 10.0,
percent_b: 0.5,
last_update: Utc::now(),
};
assert!(state.upper_band > state.middle_band);
assert!(state.middle_band > state.lower_band);
assert_eq!(state.bandwidth, 10.0);
}
#[test]
fn test_order_book_state() {
let state = OrderBookState {
timestamp: Utc::now(),
best_bid: 99.5,
best_ask: 100.5,
bid_size: 1000.0,
ask_size: 800.0,
spread: 1.0,
mid_price: 100.0,
};
assert_eq!(state.best_ask - state.best_bid, state.spread);
assert_eq!((state.best_bid + state.best_ask) / 2.0, state.mid_price);
}
#[test]
fn test_trade_data() {
let trade = TradeData {
timestamp: Utc::now(),
price: 100.0,
size: 100.0,
direction: TradeDirection::Buy,
conditions: vec!["RegularSale".to_string()],
};
assert_eq!(trade.price, 100.0);
assert_eq!(trade.size, 100.0);
assert!(matches!(trade.direction, TradeDirection::Buy));
}
#[test]
fn test_quote_data() {
let quote = QuoteData {
timestamp: Utc::now(),
bid_price: 99.5,
ask_price: 100.5,
bid_size: 1000.0,
ask_size: 800.0,
exchange: "NYSE".to_string(),
};
assert!(quote.ask_price > quote.bid_price);
assert_eq!(quote.exchange, "NYSE");
}
#[test]
fn test_tlob_analyzer_creation() {
let config = TLOBConfig {
depth_levels: 10,
enable_imbalance: true,
enable_pressure: true,
window_size: 100,
};
let analyzer = TLOBAnalyzer::new(config);
assert!(analyzer.snapshots.is_empty());
}
#[test]
fn test_tlob_snapshot() {
let snapshot = TLOBSnapshot {
timestamp: Utc::now(),
bid_levels: vec![(99.5, 1000.0), (99.0, 1500.0)],
ask_levels: vec![(100.5, 800.0), (101.0, 1200.0)],
mid_price: 100.0,
weighted_mid: 99.9,
imbalance: 0.2,
};
assert_eq!(snapshot.bid_levels.len(), 2);
assert_eq!(snapshot.ask_levels.len(), 2);
assert_eq!(snapshot.mid_price, 100.0);
}
#[test]
fn test_order_flow_event() {
let event = OrderFlowEvent {
timestamp: Utc::now(),
event_type: OrderFlowEventType::NewOrder,
price: 100.0,
size: 100.0,
side: "buy".to_string(),
};
assert!(matches!(event.event_type, OrderFlowEventType::NewOrder));
assert_eq!(event.side, "buy");
}
#[test]
fn test_temporal_features_market_hours() {
use chrono::TimeZone;
// Test regular hours (10:00 AM EST = 15:00 UTC)
let regular_hours = Utc.with_ymd_and_hms(2024, 1, 15, 15, 0, 0).unwrap();
let features = TemporalFeatures::extract_features(regular_hours);
assert_eq!(features.get("is_regular_hours"), Some(&1.0));
assert_eq!(features.get("is_premarket"), Some(&0.0));
assert_eq!(features.get("is_aftermarket"), Some(&0.0));
}
#[test]
fn test_temporal_features_premarket() {
use chrono::TimeZone;
// Test premarket (8:00 AM EST = 13:00 UTC)
let premarket = Utc.with_ymd_and_hms(2024, 1, 15, 13, 0, 0).unwrap();
let features = TemporalFeatures::extract_features(premarket);
assert_eq!(features.get("is_premarket"), Some(&1.0));
assert_eq!(features.get("is_regular_hours"), Some(&0.0));
}
#[test]
fn test_temporal_features_quarter_end() {
use chrono::TimeZone;
// Test quarter end (March 31)
let quarter_end = Utc.with_ymd_and_hms(2024, 3, 31, 12, 0, 0).unwrap();
let features = TemporalFeatures::extract_features(quarter_end);
assert_eq!(features.get("is_quarter_end"), Some(&1.0));
assert_eq!(features.get("is_month_end"), Some(&1.0));
}
#[test]
fn test_regime_detector_creation() {
let config = RegimeDetectorConfig {
lookback_periods: 50,
volatility_threshold: 0.02,
trend_threshold: 0.01,
correlation_threshold: 0.7,
rebalance_frequency: 100,
};
let detector = RegimeDetector::new(config);
assert!(detector.price_history.is_empty());
}
#[test]
fn test_portfolio_analyzer_creation() {
let config = PortfolioAnalyzerConfig {
risk_free_rate: 0.03,
target_return: 0.10,
rebalance_threshold: 0.05,
max_position_size: 0.20,
diversification_target: 10,
};
let analyzer = PortfolioAnalyzer::new(config);
assert!(analyzer.positions.is_empty());
}
#[test]
fn test_position_creation() {
let position = Position {
symbol: "AAPL".to_string(),
quantity: 100.0,
entry_price: 150.0,
current_price: 155.0,
entry_time: Utc::now(),
last_update: Utc::now(),
};
let pnl = (position.current_price - position.entry_price) * position.quantity;
assert_eq!(pnl, 500.0); // (155 - 150) * 100
}
#[test]
fn test_pnl_point() {
let pnl = PnLPoint {
timestamp: Utc::now(),
realized_pnl: 1000.0,
unrealized_pnl: 500.0,
total_pnl: 1500.0,
cumulative_pnl: 5000.0,
};
assert_eq!(pnl.realized_pnl + pnl.unrealized_pnl, pnl.total_pnl);
}
#[test]
fn test_risk_metrics() {
let metrics = RiskMetrics {
var_95: 10000.0,
var_99: 15000.0,
expected_shortfall: 18000.0,
sharpe_ratio: 1.5,
sortino_ratio: 2.0,
max_drawdown: 0.15,
volatility: 0.02,
};
assert!(metrics.var_99 > metrics.var_95);
assert!(metrics.expected_shortfall > metrics.var_99);
assert!(metrics.sortino_ratio > metrics.sharpe_ratio);
}
#[test]
fn test_spread_metrics() {
let metrics = SpreadMetrics {
bid_ask_spread: 0.01,
relative_spread: 0.0001,
effective_spread: 0.005,
realized_spread: 0.003,
price_impact: 0.002,
};
assert!(metrics.bid_ask_spread > metrics.effective_spread);
assert!(metrics.effective_spread > metrics.realized_spread);
}
#[test]
fn test_feature_category_ordering() {
assert!(FeatureCategory::Price < FeatureCategory::Volume);
assert!(FeatureCategory::TechnicalIndicator < FeatureCategory::Microstructure);
}
}