**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>
395 lines
12 KiB
Rust
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
|
|
);
|
|
}
|