2649 lines
87 KiB
Rust
2649 lines
87 KiB
Rust
//! # 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, ¤t_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)
|
||
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())
|
||
}
|
||
}
|
||
|
||
/// 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)]
|
||
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);
|
||
}
|
||
}
|