Files
foxhunt/common/tests/volume_indicators_integration_test.rs
jgrusewski 61801cfd06 feat(deprecation): Complete deprecated code analysis and cleanup preparation
**Wave D Phase 6 - Technical Debt Cleanup (Agent C6)**

## Changes
- Identified deprecated code patterns across codebase
- Analyzed mock repository usage (strategically retained per AGENT_M13)
- Documented deprecation cleanup strategy
- Prepared deprecation removal todos

## Analysis Results
- Mock structs: RETAINED (strategic testing infrastructure)
- Never-read fields: 2 instances in backtesting_service
- Dead code warnings: 35 total across workspace
- databento_old references: None found in active code

## Status
-  Deprecation analysis complete
-  Cleanup execution pending user confirmation
- 📊 Test impact assessment ready

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 00:46:19 +02:00

395 lines
12 KiB
Rust

//! Integration tests for volume-based technical indicators (OBV, MFI, VWAP)
//!
//! This test suite validates the implementation of volume indicators added in Wave 19.1.3
use chrono::Utc;
use common::ml_strategy::MLFeatureExtractor;
#[test]
fn test_obv_accumulation_uptrend() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Simulate strong uptrend with increasing volume
for i in 0..20 {
let price = 100.0 + (i as f64 * 2.0);
let volume = 1000.0 + (i as f64 * 50.0);
let features = extractor.extract_features(price, volume, timestamp);
if i >= 1 {
// OBV is at index 10 (7 base + 3 oscillators)
let obv = features[10];
// OBV should be positive in sustained uptrend
assert!(
obv > 0.0 || i == 1,
"OBV should be positive in uptrend at iteration {}, got {}",
i,
obv
);
}
}
}
#[test]
fn test_obv_distribution_downtrend() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Simulate strong downtrend
for i in 0..20 {
let price = 140.0 - (i as f64 * 2.0);
let volume = 1000.0 + (i as f64 * 50.0);
let features = extractor.extract_features(price, volume, timestamp);
if i >= 1 {
let obv = features[10];
// OBV should be negative in sustained downtrend
assert!(
obv < 0.0 || i == 1,
"OBV should be negative in downtrend at iteration {}, got {}",
i,
obv
);
}
}
}
#[test]
fn test_obv_unchanged_on_flat_price() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Extract first feature to initialize
extractor.extract_features(100.0, 1000.0, timestamp);
// Same price, different volumes - OBV should remain unchanged
let features1 = extractor.extract_features(100.0, 1500.0, timestamp);
let features2 = extractor.extract_features(100.0, 2000.0, timestamp);
let features3 = extractor.extract_features(100.0, 500.0, timestamp);
let obv1 = features1[10];
let obv2 = features2[10];
let obv3 = features3[10];
// All OBV values should be equal when price is flat
assert_eq!(obv1, obv2, "OBV should not change when price is unchanged");
assert_eq!(obv2, obv3, "OBV should not change when price is unchanged");
}
#[test]
fn test_mfi_overbought_condition() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Generate 15+ bars for MFI calculation
// Strong sustained uptrend with high volume = overbought
for i in 0..16 {
let price = 100.0 + (i as f64 * 3.0);
let volume = 1000.0 + (i as f64 * 200.0);
extractor.extract_features(price, volume, timestamp);
}
// Final strong up move
let features = extractor.extract_features(148.0, 4000.0, timestamp);
// MFI is at index 11 (7 base + 3 oscillators + OBV)
let mfi = features[11];
// MFI should be strongly positive (overbought, normalized from high MFI value)
assert!(
mfi > 0.3,
"MFI should indicate overbought condition (positive), got {}",
mfi
);
}
#[test]
fn test_mfi_oversold_condition() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Generate 15+ bars for MFI calculation
// Strong sustained downtrend with high volume = oversold
for i in 0..16 {
let price = 148.0 - (i as f64 * 3.0);
let volume = 1000.0 + (i as f64 * 200.0);
extractor.extract_features(price, volume, timestamp);
}
// Final strong down move
let features = extractor.extract_features(100.0, 4000.0, timestamp);
let mfi = features[11];
// MFI should be strongly negative (oversold, normalized from low MFI value)
assert!(
mfi < -0.3,
"MFI should indicate oversold condition (negative), got {}",
mfi
);
}
#[test]
fn test_mfi_neutral_condition() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Generate mixed market with equal buying/selling pressure
for i in 0..15 {
let price = if i % 2 == 0 { 100.0 } else { 101.0 };
let volume = 1000.0;
extractor.extract_features(price, volume, timestamp);
}
let features = extractor.extract_features(100.5, 1000.0, timestamp);
let mfi = features[11];
// MFI should be near neutral (close to 0)
assert!(
mfi.abs() < 0.5,
"MFI should be near neutral with mixed signals, got {}",
mfi
);
}
#[test]
fn test_vwap_benchmark_oscillating_market() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Trade around a base price with varying volumes
let base_price = 100.0;
let prices = vec![100.0, 102.0, 98.0, 101.0, 99.0, 100.0, 103.0, 97.0];
let volumes = vec![1000.0, 500.0, 1500.0, 800.0, 1200.0, 1000.0, 600.0, 1400.0];
for (price, volume) in prices.iter().zip(volumes.iter()) {
extractor.extract_features(*price, *volume, timestamp);
}
let features = extractor.extract_features(100.0, 1000.0, timestamp);
// VWAP is at index 12 (7 base + 3 oscillators + OBV + MFI)
let vwap_ratio = features[12];
// VWAP ratio should be near 0 when price oscillates around average
assert!(
vwap_ratio.abs() < 0.2,
"VWAP ratio should be near 0 for oscillating prices, got {}",
vwap_ratio
);
}
#[test]
fn test_vwap_below_current_price() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// High volume at low prices, then price rises with low volume
extractor.extract_features(100.0, 5000.0, timestamp);
extractor.extract_features(101.0, 4000.0, timestamp);
extractor.extract_features(102.0, 3000.0, timestamp);
// Price jumps up with low volume
let features = extractor.extract_features(110.0, 500.0, timestamp);
let vwap_ratio = features[12];
// Price > VWAP, so ratio should be positive (bullish)
assert!(
vwap_ratio > 0.0,
"VWAP ratio should be positive when price > VWAP, got {}",
vwap_ratio
);
}
#[test]
fn test_vwap_above_current_price() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// High volume at high prices, then price drops with low volume
extractor.extract_features(110.0, 5000.0, timestamp);
extractor.extract_features(109.0, 4000.0, timestamp);
extractor.extract_features(108.0, 3000.0, timestamp);
// Price drops with low volume
let features = extractor.extract_features(100.0, 500.0, timestamp);
let vwap_ratio = features[12];
// Price < VWAP, so ratio should be negative (bearish)
assert!(
vwap_ratio < 0.0,
"VWAP ratio should be negative when price < VWAP, got {}",
vwap_ratio
);
}
#[test]
fn test_all_volume_indicators_normalized() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Generate diverse market conditions to test normalization
for i in 0..20 {
let price = 100.0 + ((i as f64 * 5.0).sin() * 20.0); // Volatile sine wave
let volume = 500.0 + (i as f64 * 100.0); // Increasing volume
extractor.extract_features(price, volume, timestamp);
}
let features = extractor.extract_features(105.0, 2500.0, timestamp);
let obv = features[10];
let mfi = features[11];
let vwap = features[12];
// All volume indicators should be in [-1, 1] range
assert!(
obv >= -1.0 && obv <= 1.0,
"OBV should be normalized to [-1, 1], got {}",
obv
);
assert!(
mfi >= -1.0 && mfi <= 1.0,
"MFI should be normalized to [-1, 1], got {}",
mfi
);
assert!(
vwap >= -1.0 && vwap <= 1.0,
"VWAP should be normalized to [-1, 1], got {}",
vwap
);
}
#[test]
fn test_volume_indicators_with_extreme_values() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Test with extreme volume spikes and price movements
for i in 0..15 {
let price = if i == 10 { 150.0 } else { 100.0 }; // Price spike
let volume = if i == 10 { 50000.0 } else { 1000.0 }; // Volume spike
extractor.extract_features(price, volume, timestamp);
}
let features = extractor.extract_features(102.0, 1200.0, timestamp);
let obv = features[10];
let mfi = features[11];
let vwap = features[12];
// Even with extreme values, indicators should remain normalized
assert!(
obv >= -1.0 && obv <= 1.0,
"OBV should handle extreme values, got {}",
obv
);
assert!(
mfi >= -1.0 && mfi <= 1.0,
"MFI should handle extreme values, got {}",
mfi
);
assert!(
vwap >= -1.0 && vwap <= 1.0,
"VWAP should handle extreme values, got {}",
vwap
);
}
#[test]
fn test_volume_indicators_insufficient_data() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Test with minimal data points
let features1 = extractor.extract_features(100.0, 1000.0, timestamp);
let features2 = extractor.extract_features(101.0, 1100.0, timestamp);
// OBV should work with 2 data points
assert_eq!(features1[10], 0.0, "OBV should be 0 for first data point");
// MFI should default to 0 with insufficient data (needs 15 points)
assert_eq!(features1[11], 0.0, "MFI should be 0 with insufficient data");
assert_eq!(features2[11], 0.0, "MFI should be 0 with insufficient data");
// VWAP should work with any amount of data
assert!(
features1[12] >= -1.0 && features1[12] <= 1.0,
"VWAP should be calculated even with minimal data"
);
}
#[test]
fn test_feature_vector_includes_volume_indicators() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Generate sufficient data for all indicators
for i in 0..30 {
let price = 100.0 + (i as f64 * 0.5);
let volume = 1000.0 + (i as f64 * 10.0);
extractor.extract_features(price, volume, timestamp);
}
let features = extractor.extract_features(115.0, 1300.0, timestamp);
// Total features: 30 (Wave A + Wave C)
// Wave A: 26 features (7 base + 3 oscillators + 3 volume + 5 EMA + 1 ADX + 1 BB + 2 Stoch + 1 CCI + 1 RSI + 2 MACD)
// Wave C: 4 features (OBV Momentum, Volume Oscillator, A/D Line, EMA Ratio)
assert_eq!(
features.len(),
30,
"Feature vector should include all 30 features (Wave A + Wave C)"
);
// Verify volume indicators are at correct indices
let obv = features[10];
let mfi = features[11];
let vwap = features[12];
assert!(obv.abs() <= 1.0, "OBV at index 10");
assert!(mfi.abs() <= 1.0, "MFI at index 11");
assert!(vwap.abs() <= 1.0, "VWAP at index 12");
}
#[test]
fn test_volume_indicators_provide_unique_signals() {
let mut extractor = MLFeatureExtractor::new(30);
let timestamp = Utc::now();
// Create scenario where volume indicators should diverge
// Phase 1: High volume accumulation at low prices
for i in 0..10 {
let price = 100.0 - (i as f64 * 0.5);
let volume = 1000.0 + (i as f64 * 300.0); // Increasing volume
extractor.extract_features(price, volume, timestamp);
}
// Phase 2: Price recovery with moderate volume
for i in 0..10 {
let price = 95.0 + (i as f64 * 1.0);
let volume = 1500.0; // Consistent moderate volume
extractor.extract_features(price, volume, timestamp);
}
let features = extractor.extract_features(105.0, 1600.0, timestamp);
let obv = features[10];
let mfi = features[11];
let vwap = features[12];
// All three indicators should provide different perspectives
// OBV: Should reflect volume accumulation during downturn + recovery
// MFI: Should show recent buying pressure (14-period window)
// VWAP: Should show price relative to volume-weighted average
// Verify they're not all the same (they provide unique information)
let indicators_equal = (obv - mfi).abs() < 0.01 && (mfi - vwap).abs() < 0.01;
assert!(
!indicators_equal,
"Volume indicators should provide different signals: OBV={}, MFI={}, VWAP={}",
obv, mfi, vwap
);
}