Files
foxhunt/ml/src/features/volume_features.rs
jgrusewski 1934367bfa refactor(ml): consolidate 13 duplicate OHLCVBar definitions into single canonical type
Created ml/src/types/ohlcv.rs as the single source of truth for OHLCVBar
(DateTime<Utc> timestamp, f64 OHLCV fields). Replaced all 13 duplicate
definitions across features/, regime/, real_data_loader, and evaluation/
with imports from crate::types::OHLCVBar.

Key changes:
- New: ml/src/types/mod.rs + ohlcv.rs with canonical OHLCVBar
  (derives: Debug, Clone, Copy, PartialEq, Serialize, Deserialize + Default)
- Renamed: evaluation::metrics::OHLCVBar → OHLCVBarF32 (genuinely
  different type: f32 fields, i64 timestamp for compact backtesting)
- Eliminated all import aliases (ExtractionOHLCVBar, RegimeOHLCVBar,
  PriceOHLCVBar, VolumeOHLCVBar) in dbn_sequence_loader.rs and pipeline.rs
- Renamed regime::orchestrator::Bar → OHLCVBar (same fields, just aliased)
- Updated 39 files total (13 definitions removed, imports normalized)

1883 lib tests passing, compilation clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:14:42 +01:00

883 lines
26 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.
//! Volume-Based Features for Wave C Feature Engineering
//!
//! This module implements 10 advanced volume features to complement the existing
//! 40 volume features in extraction.rs. These features capture volume dynamics,
//! price-volume relationships, and market participation patterns.
//!
//! ## Features Implemented (Indices 256-265)
//! 1. Volume Ratio to SMA-50 (256)
//! 2. Volume ROC 5-period (257)
//! 3. Volume ROC 10-period (258)
//! 4. Volume Acceleration (259)
//! 5. Volume Trend Slope (260)
//! 6. VWAP Intraday Deviation (261)
//! 7. Volume-Price Correlation (262)
//! 8. Volume Percentile 10-period (263)
//! 9. Volume Concentration HHI (264)
//! 10. Volume Imbalance Buy/Sell (265)
//!
//! ## Performance Target
//! - Latency: <150μs for all 10 features per bar
//! - Memory: <100 bytes per bar (reuses existing VecDeque)
//!
//! ## Integration
//! These features extend the 256-dimension feature vector to 266 dimensions.
//!
//! ## References
//! - WAVE_C_VOLUME_FEATURES_DESIGN.md (comprehensive design document)
//! - ml/src/features/extraction.rs (existing 40 volume features)
use anyhow::Result;
use std::collections::VecDeque;
pub use crate::types::OHLCVBar;
/// Volume feature extractor with stateful rolling windows (Wave G17 optimized)
///
/// Memory Optimization:
/// - OLD: `VecDeque<OHLCVBar>` with capacity 260 → ~32.5KB per symbol (always allocated)
/// - NEW: `Option<VecDeque<OHLCVBar>>` → 0 bytes until first bar (lazy allocated)
/// - Savings: ~32KB per symbol × 100K symbols (for unused symbols) = 3.2 GB
///
/// Note: Lazy allocation via Option reduces memory for unused symbols to zero.
#[derive(Debug)]
pub struct VolumeFeatureExtractor {
/// Rolling window of bars (Wave G17: lazy allocation for 100% savings on unused symbols)
bars: Option<VecDeque<OHLCVBar>>,
}
impl VolumeFeatureExtractor {
/// Creates a new volume feature extractor (Wave G17: lazy allocation)
pub fn new() -> Self {
Self { bars: None }
}
/// Updates the extractor with a new bar
pub fn update(&mut self, bar: &OHLCVBar) {
let buffer = self
.bars
.get_or_insert_with(|| VecDeque::with_capacity(260));
buffer.push_back(*bar);
if buffer.len() > 260 {
buffer.pop_front();
}
}
/// Helper: Get bars reference
fn bars(&self) -> &VecDeque<OHLCVBar> {
static EMPTY: once_cell::sync::Lazy<VecDeque<OHLCVBar>> =
once_cell::sync::Lazy::new(VecDeque::new);
self.bars.as_ref().unwrap_or(&EMPTY)
}
/// Extracts all 10 volume features (indices 256-265)
///
/// ## Returns
/// - Array of 10 features: [256, 257, ..., 265]
///
/// ## Performance
/// - Target: <150μs per call
/// - Complexity: O(1) amortized for most features, O(n) for correlation/HHI
pub fn extract_features(&self) -> Result<[f64; 10]> {
let mut features = [0.0; 10];
// Feature 256: Volume ratio to SMA-50
features[0] = self.compute_volume_ratio_sma50();
// Feature 257: Volume ROC 5-period
features[1] = self.compute_volume_roc(5);
// Feature 258: Volume ROC 10-period
features[2] = self.compute_volume_roc(10);
// Feature 259: Volume acceleration
features[3] = self.compute_volume_acceleration();
// Feature 260: Volume trend slope (20-period linear regression)
features[4] = self.compute_volume_trend_slope(20);
// Feature 261: VWAP intraday deviation
features[5] = self.compute_vwap_deviation();
// Feature 262: Volume-price correlation (20-period)
features[6] = self.compute_volume_price_correlation(20);
// Feature 263: Volume percentile (10-period)
features[7] = self.compute_volume_percentile(10);
// Feature 264: Volume concentration HHI (20-period)
features[8] = self.compute_volume_concentration_hhi(20);
// Feature 265: Volume imbalance (5-period buy/sell)
features[9] = self.compute_volume_imbalance(5);
// Validate no NaN/Inf
for (i, &val) in features.iter().enumerate() {
if !val.is_finite() {
anyhow::bail!("Invalid volume feature at index {}: {}", i + 256, val);
}
}
Ok(features)
}
// ===== Feature Implementation Methods =====
/// Feature 256: Volume ratio to SMA-50
///
/// Formula: (current_volume - sma_50) / sma_50
/// Range: [-2.0, 5.0]
fn compute_volume_ratio_sma50(&self) -> f64 {
if self.bars().len() < 50 {
return 0.0;
}
let bar = self.bars().back().unwrap();
let sma_50 = self.compute_volume_sma(50);
let ratio = (bar.volume - sma_50) / (sma_50 + 1e-8);
safe_clip(ratio, -2.0, 5.0)
}
/// Feature 257/258: Volume ROC (Rate of Change)
///
/// Formula: (current_volume - volume_n_bars_ago) / volume_n_bars_ago
/// Range: [-1.0, 3.0]
fn compute_volume_roc(&self, period: usize) -> f64 {
let len = self.bars().len();
if len <= period {
return 0.0;
}
let curr_vol = self.bars().back().unwrap().volume;
let prev_vol = self.bars()[len - period - 1].volume;
let roc = (curr_vol - prev_vol) / (prev_vol + 1e-8);
safe_clip(roc, -1.0, 3.0)
}
/// Feature 259: Volume acceleration (second derivative)
///
/// Formula: (velocity_1 - velocity_2) / 1000
/// Range: [-5.0, 5.0]
fn compute_volume_acceleration(&self) -> f64 {
if self.bars().len() < 3 {
return 0.0;
}
let curr = self.bars().back().unwrap().volume;
let prev1 = self.bars()[self.bars().len() - 2].volume;
let prev2 = self.bars()[self.bars().len() - 3].volume;
let vel1 = curr - prev1;
let vel2 = prev1 - prev2;
let accel = vel1 - vel2;
safe_clip(accel / 1000.0, -5.0, 5.0)
}
/// Feature 260: Volume trend slope (linear regression)
///
/// Formula: Linear regression slope over period
/// Range: [-1.0, 1.0]
fn compute_volume_trend_slope(&self, period: usize) -> f64 {
if self.bars().len() < period {
return 0.0;
}
let start = self.bars().len() - period;
let n = period as f64;
// Linear regression formula: slope = (n*Σxy - Σx*Σy) / (n*Σx² - (Σx)²)
let sum_x = (n * (n - 1.0)) / 2.0;
let sum_x2 = (n * (n - 1.0) * (2.0 * n - 1.0)) / 6.0;
let mut sum_y = 0.0;
let mut sum_xy = 0.0;
for (i, bar) in self.bars().iter().skip(start).enumerate() {
sum_y += bar.volume;
sum_xy += i as f64 * bar.volume;
}
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
safe_clip(slope / 100.0, -1.0, 1.0)
}
/// Feature 261: VWAP intraday deviation
///
/// Formula: (close - vwap) / close
/// Range: [-0.1, 0.1]
fn compute_vwap_deviation(&self) -> f64 {
if self.bars().len() < 20 {
return 0.0;
}
let bar = self.bars().back().unwrap();
let vwap = self.compute_vwap(20);
let deviation = (bar.close - vwap) / (bar.close + 1e-8);
safe_clip(deviation, -0.1, 0.1)
}
/// Feature 262: Volume-price correlation (Pearson)
///
/// Formula: Pearson correlation coefficient
/// Range: [-1.0, 1.0]
fn compute_volume_price_correlation(&self, period: usize) -> f64 {
if self.bars().len() < period {
return 0.0;
}
let start = self.bars().len() - period;
let prices: Vec<f64> = self.bars().iter().skip(start).map(|b| b.close).collect();
let volumes: Vec<f64> = self.bars().iter().skip(start).map(|b| b.volume).collect();
self.compute_correlation(&prices, &volumes)
}
/// Feature 263: Volume percentile rank
///
/// Formula: count(vol < current_vol) / period
/// Range: [0.0, 1.0]
fn compute_volume_percentile(&self, period: usize) -> f64 {
if self.bars().len() < period {
return 0.5; // Neutral
}
let current_vol = self.bars().back().unwrap().volume;
let start = self.bars().len() - period;
let count_below = self
.bars()
.iter()
.skip(start)
.filter(|b| b.volume < current_vol)
.count();
count_below as f64 / period as f64
}
/// Feature 264: Volume concentration (Herfindahl-Hirschman Index)
///
/// Formula: HHI = Σ(vol_i / total_vol)²
/// Range: [0.0, 1.0] (normalized from [1/n, 1])
fn compute_volume_concentration_hhi(&self, period: usize) -> f64 {
if self.bars().len() < period {
return 0.5; // Neutral
}
let start = self.bars().len() - period;
let total_vol: f64 = self.bars().iter().skip(start).map(|b| b.volume).sum();
if total_vol < 1e-8 {
return 0.5; // Neutral for zero volume
}
let hhi: f64 = self
.bars()
.iter()
.skip(start)
.map(|b| {
let share = b.volume / total_vol;
share * share
})
.sum();
// Normalize: HHI ∈ [1/n, 1] → [0, 1]
let min_hhi = 1.0 / period as f64;
let normalized = (hhi - min_hhi) / (1.0 - min_hhi);
safe_clip(normalized, 0.0, 1.0)
}
/// Feature 265: Volume imbalance (buy vs sell pressure)
///
/// Formula: (buy_vol - sell_vol) / total_vol
/// Range: [-1.0, 1.0]
fn compute_volume_imbalance(&self, period: usize) -> f64 {
if self.bars().len() < period {
return 0.0;
}
let start = self.bars().len() - period;
let mut buy_vol = 0.0;
let mut sell_vol = 0.0;
for bar in self.bars().iter().skip(start) {
if bar.close > bar.open {
buy_vol += bar.volume;
} else if bar.close < bar.open {
sell_vol += bar.volume;
}
// Doji bars (close == open) contribute to neither
}
let total_vol = buy_vol + sell_vol + 1e-8;
let imbalance = (buy_vol - sell_vol) / total_vol;
safe_clip(imbalance, -1.0, 1.0)
}
// ===== Helper Methods (reuse extraction.rs patterns) =====
fn compute_volume_sma(&self, period: usize) -> f64 {
let start = self.bars().len().saturating_sub(period);
let sum: f64 = self.bars().iter().skip(start).map(|b| b.volume).sum();
sum / period as f64
}
fn compute_vwap(&self, period: usize) -> f64 {
let start = self.bars().len().saturating_sub(period);
let (weighted_sum, volume_sum): (f64, f64) = self
.bars()
.iter()
.skip(start)
.map(|b| (b.close * b.volume, b.volume))
.fold((0.0, 0.0), |(ws, vs), (w, v)| (ws + w, vs + v));
weighted_sum / (volume_sum + 1e-8)
}
fn compute_correlation(&self, x: &[f64], y: &[f64]) -> f64 {
if x.len() != y.len() || x.is_empty() {
return 0.0;
}
let n = x.len() as f64;
let mean_x: f64 = x.iter().sum::<f64>() / n;
let mean_y: f64 = y.iter().sum::<f64>() / n;
let mut cov = 0.0;
let mut var_x = 0.0;
let mut var_y = 0.0;
for i in 0..x.len() {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
cov += dx * dy;
var_x += dx * dx;
var_y += dy * dy;
}
let denom = (var_x * var_y).sqrt();
if denom > 1e-8 {
safe_clip(cov / denom, -1.0, 1.0)
} else {
0.0
}
}
}
impl Default for VolumeFeatureExtractor {
fn default() -> Self {
Self::new()
}
}
// ===== Utility Functions =====
/// Safe clipping: Clip value to [min, max] range, handles NaN/Inf
fn safe_clip(value: f64, min: f64, max: f64) -> f64 {
if !value.is_finite() {
return 0.0;
}
value.clamp(min, max)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn create_bars_with_volume(volumes: Vec<f64>) -> Vec<OHLCVBar> {
volumes
.iter()
.enumerate()
.map(|(i, &vol)| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i as i64),
open: 100.0,
high: 101.0,
low: 99.0,
close: 100.5,
volume: vol,
})
.collect()
}
fn create_bars_with_price_volume(prices: Vec<f64>, volumes: Vec<f64>) -> Vec<OHLCVBar> {
prices
.iter()
.zip(volumes.iter())
.enumerate()
.map(|(i, (&p, &v))| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i as i64),
open: p,
high: p + 1.0,
low: p - 1.0,
close: p,
volume: v,
})
.collect()
}
fn create_bars_with_ohlc(ohlc: Vec<(f64, f64)>, volumes: Vec<f64>) -> Vec<OHLCVBar> {
ohlc.iter()
.zip(volumes.iter())
.enumerate()
.map(|(i, (&(o, c), &v))| OHLCVBar {
timestamp: Utc::now() + chrono::Duration::hours(i as i64),
open: o,
high: o.max(c) + 1.0,
low: o.min(c) - 1.0,
close: c,
volume: v,
})
.collect()
}
#[test]
fn test_volume_ratio_normal() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0; 51]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[0] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[0]
);
}
#[test]
fn test_volume_ratio_2x_spike() {
let mut extractor = VolumeFeatureExtractor::new();
let mut volumes = vec![1000.0; 50];
volumes.push(2000.0);
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
// SMA-50 = (49*1000 + 2000) / 50 = 1020
// Ratio = (2000 - 1020) / 1020 = 0.96
assert!(
(features[0] - 0.96).abs() < 0.02,
"Expected 0.96, got {}",
features[0]
);
}
#[test]
fn test_volume_ratio_extreme_clipping() {
let mut extractor = VolumeFeatureExtractor::new();
let mut volumes = vec![1000.0; 50];
volumes.push(10000.0);
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[0] - 5.0).abs() < 0.01,
"Expected 5.0 (clipped), got {}",
features[0]
);
}
#[test]
fn test_volume_roc_5_flat() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0; 10]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[1] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[1]
);
}
#[test]
fn test_volume_roc_5_doubling() {
let mut extractor = VolumeFeatureExtractor::new();
let volumes = vec![1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 2000.0];
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[1] - 1.0).abs() < 0.01,
"Expected 1.0, got {}",
features[1]
);
}
#[test]
fn test_volume_acceleration_constant() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0, 1100.0, 1200.0]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[3] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[3]
);
}
#[test]
fn test_volume_acceleration_positive() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0, 1100.0, 1300.0]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
features[3] > 0.0,
"Expected positive acceleration, got {}",
features[3]
);
}
#[test]
fn test_volume_trend_flat() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0; 25]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[4] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[4]
);
}
#[test]
fn test_volume_trend_uptrend() {
let mut extractor = VolumeFeatureExtractor::new();
let volumes: Vec<f64> = (1000..1025).map(|x| x as f64 * 100.0).collect();
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
features[4] > 0.0,
"Expected positive slope, got {}",
features[4]
);
}
#[test]
fn test_vwap_at_fair_value() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_price_volume(vec![100.0; 25], vec![1000.0; 25]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[5] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[5]
);
}
#[test]
fn test_volume_price_correlation_positive() {
let mut extractor = VolumeFeatureExtractor::new();
let prices: Vec<f64> = (100..120).map(|x| x as f64).collect();
let volumes: Vec<f64> = (1000..1020).map(|x| x as f64 * 100.0).collect();
let bars = create_bars_with_price_volume(prices, volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
features[6] > 0.5,
"Expected strong positive correlation, got {}",
features[6]
);
}
#[test]
fn test_volume_price_correlation_negative() {
let mut extractor = VolumeFeatureExtractor::new();
let prices: Vec<f64> = (100..120).rev().map(|x| x as f64).collect();
let volumes: Vec<f64> = (1000..1020).map(|x| x as f64 * 100.0).collect();
let bars = create_bars_with_price_volume(prices, volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
features[6] < -0.5,
"Expected strong negative correlation, got {}",
features[6]
);
}
#[test]
fn test_volume_percentile_minimum() {
let mut extractor = VolumeFeatureExtractor::new();
let mut volumes = vec![1000.0; 10];
volumes[9] = 500.0;
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[7] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[7]
);
}
#[test]
fn test_volume_percentile_maximum() {
let mut extractor = VolumeFeatureExtractor::new();
let mut volumes = vec![1000.0; 10];
volumes[9] = 2000.0;
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[7] - 1.0).abs() < 0.11,
"Expected 1.0, got {}",
features[7]
);
}
#[test]
fn test_volume_concentration_uniform() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0; 25]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[8] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[8]
);
}
#[test]
fn test_volume_concentration_high() {
let mut extractor = VolumeFeatureExtractor::new();
// Create more extreme concentration: 19 very small + 1 dominant volume
let mut volumes = vec![10.0; 19];
volumes.push(9900.0);
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
// Total = 19*10 + 9900 = 10090
// HHI = 19*(10/10090)² + (9900/10090)² ≈ 0.000019 + 0.963 = 0.963
// min_hhi = 1/20 = 0.05
// normalized = (0.963 - 0.05) / (1 - 0.05) = 0.96
assert!(
features[8] > 0.9,
"Expected high HHI (>0.9), got {}",
features[8]
);
}
#[test]
fn test_volume_imbalance_balanced() {
let mut extractor = VolumeFeatureExtractor::new();
let ohlc = vec![(100.0, 100.0); 5]; // Doji bars
let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[9] - 0.0).abs() < 0.01,
"Expected 0.0, got {}",
features[9]
);
}
#[test]
fn test_volume_imbalance_buying() {
let mut extractor = VolumeFeatureExtractor::new();
let ohlc = vec![(100.0, 110.0); 5]; // All bullish bars
let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[9] - 1.0).abs() < 0.01,
"Expected 1.0, got {}",
features[9]
);
}
#[test]
fn test_volume_imbalance_selling() {
let mut extractor = VolumeFeatureExtractor::new();
let ohlc = vec![(110.0, 100.0); 5]; // All bearish bars
let bars = create_bars_with_ohlc(ohlc, vec![1000.0; 5]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
assert!(
(features[9] - -1.0).abs() < 0.01,
"Expected -1.0, got {}",
features[9]
);
}
#[test]
fn test_insufficient_history_returns_default() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1000.0; 3]);
for bar in &bars {
extractor.update(bar);
}
// Should succeed but return mostly 0.0 values
let features = extractor.extract_features().unwrap();
// Most features should be 0.0 or neutral (0.5 for percentile/HHI)
assert!((features[0] - 0.0).abs() < 0.01); // Volume ratio (insufficient)
assert!((features[7] - 0.5).abs() < 0.01); // Percentile (neutral)
assert!((features[8] - 0.5).abs() < 0.01); // HHI (neutral)
}
#[test]
fn test_zero_volume_handling() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![0.0; 55]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
// All values should be finite (no NaN/Inf)
for (i, &val) in features.iter().enumerate() {
assert!(
val.is_finite(),
"Found non-finite value at index {}: {}",
i,
val
);
}
}
#[test]
fn test_extreme_volume_clipping() {
let mut extractor = VolumeFeatureExtractor::new();
let bars = create_bars_with_volume(vec![1_000_000.0; 55]);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
// All values should be within expected ranges
assert!(features[0] >= -2.0 && features[0] <= 5.0); // Volume ratio
assert!(features[1] >= -1.0 && features[1] <= 3.0); // ROC 5
assert!(features[2] >= -1.0 && features[2] <= 3.0); // ROC 10
assert!(features[3] >= -5.0 && features[3] <= 5.0); // Acceleration
assert!(features[4] >= -1.0 && features[4] <= 1.0); // Trend slope
assert!(features[5] >= -0.1 && features[5] <= 0.1); // VWAP deviation
assert!(features[6] >= -1.0 && features[6] <= 1.0); // Correlation
assert!(features[7] >= 0.0 && features[7] <= 1.0); // Percentile
assert!(features[8] >= 0.0 && features[8] <= 1.0); // HHI
assert!(features[9] >= -1.0 && features[9] <= 1.0); // Imbalance
}
#[test]
fn test_all_features_finite() {
let mut extractor = VolumeFeatureExtractor::new();
// Create diverse bars with varying volumes
let volumes = vec![
1000.0, 1200.0, 800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0,
1200.0, 800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0,
800.0, 1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0,
1500.0, 900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0, 1500.0,
900.0, 2000.0, 1100.0, 1300.0, 700.0, 1400.0, 1000.0, 1200.0, 800.0, 1500.0, 900.0,
];
let bars = create_bars_with_volume(volumes);
for bar in &bars {
extractor.update(bar);
}
let features = extractor.extract_features().unwrap();
// Validate all features are finite
for (i, &val) in features.iter().enumerate() {
assert!(
val.is_finite(),
"Feature {} is not finite: {}",
i + 256,
val
);
}
}
}