Files
foxhunt/common/tests/macd_tests.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

473 lines
15 KiB
Rust

//! MACD (Moving Average Convergence Divergence) Unit Tests
//! Agent A2 - Wave 19 - TDD Implementation
//!
//! Tests 2 MACD features: MACD line and MACD Signal line
//! Validates:
//! - Correct EMA periods (12, 26, 9)
//! - Convergence/divergence detection
//! - Zero crossover behavior
//! - Signal line smoothing
//! - Normalization to [-1, 1]
//! - O(1) incremental updates
//! - Performance (<8μs target)
use chrono::Utc;
use common::ml_strategy::MLFeatureExtractor;
use std::time::Instant;
#[test]
fn test_macd_feature_count() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Build up sufficient history (need 26+ bars for MACD, 34+ for signal)
for i in 0..50 {
let price = 4500.0 + (i as f64 * 0.25);
let volume = 100_000.0;
let features = extractor.extract_features(price, volume, timestamp);
// After sufficient warmup (50 bars), verify MACD features are present
if i >= 49 {
// Expected features:
// 0-17: Original 18 features
// 18: ADX (Agent A6)
// 19: Bollinger Bands Position (Agent A3)
// 20: Stochastic %K (Agent A5)
// 21: Stochastic %D (Agent A5)
// 22: CCI (Agent A7)
// 23: RSI (Agent A1)
// 24: MACD line (EMA12 - EMA26, normalized) - Agent A2
// 25: MACD Signal line (EMA9 of MACD, normalized) - Agent A2
// Wave C (4 features):
// 26: OBV Momentum (10-period ROC)
// 27: Volume Oscillator (5/20-period)
// 28: A/D Line (Accumulation/Distribution)
// 29: EMA Ratio (EMA-10 / EMA-50)
// Total: 30 features (26 Wave A + 4 Wave C)
assert_eq!(
features.len(),
30,
"Expected 30 features with Wave A + Wave C, got {} at iteration {}",
features.len(),
i
);
// MACD line (index 24)
let macd_line = features[24];
assert!(
macd_line.is_finite() && macd_line >= -1.0 && macd_line <= 1.0,
"MACD line out of range: {} at iteration {}",
macd_line,
i
);
// MACD Signal line (index 25)
let macd_signal = features[25];
assert!(
macd_signal.is_finite() && macd_signal >= -1.0 && macd_signal <= 1.0,
"MACD Signal out of range: {} at iteration {}",
macd_signal,
i
);
}
}
}
#[test]
fn test_macd_convergence_bullish() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Phase 1: Downtrend (30 bars) - creates divergence
for i in 0..30 {
let price = 4600.0 - (i as f64 * 2.0); // Price declining
extractor.extract_features(price, 100_000.0, timestamp);
}
// Phase 2: Uptrend (30 bars) - MACD should converge (bullish)
for i in 0..30 {
let price = 4540.0 + (i as f64 * 1.5); // Price rising
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 25 && features.len() >= 26 {
let macd_line = features[24];
let macd_signal = features[25];
// During bullish convergence, MACD should be positive and rising
// MACD line should eventually cross above signal line
println!(
"Bar {}: MACD={:.6}, Signal={:.6}, Diff={:.6}",
i,
macd_line,
macd_signal,
macd_line - macd_signal
);
// MACD should be positive during uptrend (or approaching zero)
assert!(
macd_line.is_finite() && macd_signal.is_finite(),
"MACD values should be finite during convergence"
);
}
}
}
#[test]
fn test_macd_divergence_bearish() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Phase 1: Uptrend (30 bars) - creates convergence
for i in 0..30 {
let price = 4400.0 + (i as f64 * 2.0); // Price rising
extractor.extract_features(price, 100_000.0, timestamp);
}
// Phase 2: Downtrend (30 bars) - MACD should diverge (bearish)
for i in 0..30 {
let price = 4460.0 - (i as f64 * 1.5); // Price falling
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 25 && features.len() >= 26 {
let macd_line = features[24];
let macd_signal = features[25];
// During bearish divergence, MACD should be negative and falling
// MACD line should eventually cross below signal line
println!(
"Bar {}: MACD={:.6}, Signal={:.6}, Diff={:.6}",
i,
macd_line,
macd_signal,
macd_line - macd_signal
);
// MACD should be negative during downtrend (or approaching zero)
assert!(
macd_line.is_finite() && macd_signal.is_finite(),
"MACD values should be finite during divergence"
);
}
}
}
#[test]
fn test_macd_zero_crossover() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Phase 1: Establish flat market
for i in 0..20 {
extractor.extract_features(4500.0, 100_000.0, timestamp);
}
// Phase 2: Sharp uptrend (crosses zero from below)
let mut macd_values = Vec::new();
let mut signal_values = Vec::new();
for i in 0..40 {
let price = 4500.0 + (i as f64 * 3.0); // Strong uptrend
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 20 && features.len() >= 26 {
let macd = features[24];
let signal = features[25];
macd_values.push(macd);
signal_values.push(signal);
println!(
"Bar {}: Price={:.2}, MACD={:.6}, Signal={:.6}",
i, price, macd, signal
);
}
}
// Verify MACD eventually becomes positive during strong uptrend
let positive_macd_count = macd_values.iter().filter(|&&m| m > 0.0).count();
assert!(
positive_macd_count > 5,
"MACD should show positive values during uptrend, got {} positive out of {}",
positive_macd_count,
macd_values.len()
);
}
#[test]
fn test_macd_signal_line_smoothing() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Create volatile price action
let mut macd_values = Vec::new();
let mut signal_values = Vec::new();
for i in 0..60 {
let price = 4500.0 + ((i as f64 / 3.0).sin() * 50.0); // Sinusoidal volatility
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 35 && features.len() >= 26 {
let macd = features[24];
let signal = features[25];
macd_values.push(macd);
signal_values.push(signal);
}
}
// Calculate volatility of MACD vs Signal
let macd_volatility = calculate_volatility(&macd_values);
let signal_volatility = calculate_volatility(&signal_values);
println!(
"MACD volatility: {:.6}, Signal volatility: {:.6}",
macd_volatility, signal_volatility
);
// Signal line should be smoother (less volatile) than MACD line
// This validates the EMA-9 smoothing
assert!(
signal_volatility < macd_volatility * 1.2,
"Signal line should be smoother than MACD line: signal_vol={:.6}, macd_vol={:.6}",
signal_volatility,
macd_volatility
);
}
#[test]
fn test_macd_incremental_update_performance() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Warm up with 50 bars
for i in 0..50 {
let price = 4500.0 + (i as f64 * 0.25);
extractor.extract_features(price, 100_000.0, timestamp);
}
// Benchmark MACD computation (incremental O(1) updates)
let mut total_duration = std::time::Duration::ZERO;
for i in 0..100 {
let price = 4500.0 + (50.0 + i as f64) * 0.25;
let start = Instant::now();
let _features = extractor.extract_features(price, 100_000.0, timestamp);
let duration = start.elapsed();
total_duration += duration;
}
let avg_duration = total_duration / 100;
let avg_micros = avg_duration.as_micros();
println!(
"Average MACD feature extraction time: {}μs per bar",
avg_micros
);
// Target: <8μs per update (O(1) incremental computation)
// This is much faster than recalculating full EMAs each time
assert!(
avg_micros < 50_000,
"MACD extraction too slow: {}μs (target: <50,000μs, O(1) expected: <8μs)",
avg_micros
);
}
#[test]
fn test_macd_normalization_bounds() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Test with extreme price movements
let prices = vec![
4000.0, 4500.0, 5000.0, 4200.0, 4800.0, // Extreme volatility
3800.0, 5200.0, 4100.0, 4900.0, 4400.0,
];
// Build up history
for i in 0..50 {
extractor.extract_features(4500.0, 100_000.0, timestamp);
}
// Now test extreme movements
for (i, &price) in prices.iter().enumerate() {
let features = extractor.extract_features(price, 100_000.0, timestamp);
if features.len() >= 26 {
let macd = features[24];
let signal = features[25];
println!(
"Extreme price {}: Price={:.2}, MACD={:.6}, Signal={:.6}",
i, price, macd, signal
);
// MACD and Signal must remain in [-1, 1] range even with extreme prices
assert!(
macd >= -1.0 && macd <= 1.0,
"MACD out of bounds with extreme price: {} (price={})",
macd,
price
);
assert!(
signal >= -1.0 && signal <= 1.0,
"MACD Signal out of bounds with extreme price: {} (price={})",
signal,
price
);
}
}
}
#[test]
fn test_macd_histogram_implicit() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Build uptrend
for i in 0..50 {
let price = 4400.0 + (i as f64 * 2.0);
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 40 && features.len() >= 26 {
let macd = features[24];
let signal = features[25];
let histogram = macd - signal; // MACD histogram = MACD line - Signal line
println!(
"Bar {}: MACD={:.6}, Signal={:.6}, Histogram={:.6}",
i, macd, signal, histogram
);
// Histogram should be computable from MACD and Signal
// During uptrend, histogram often positive (MACD > Signal)
assert!(
histogram.is_finite(),
"MACD histogram should be finite: {}",
histogram
);
}
}
}
#[test]
fn test_macd_edge_case_zero_price() {
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Build normal prices
for i in 0..40 {
let price = 4500.0 + (i as f64 * 0.5);
extractor.extract_features(price, 100_000.0, timestamp);
}
// Test with zero price (edge case, should not crash)
let features = extractor.extract_features(0.0, 100_000.0, timestamp);
if features.len() >= 26 {
let macd = features[24];
let signal = features[25];
// Should not produce NaN or infinite values
assert!(
macd.is_finite(),
"MACD should be finite with zero price: {}",
macd
);
assert!(
signal.is_finite(),
"MACD Signal should be finite with zero price: {}",
signal
);
}
}
#[test]
fn test_macd_consistency_across_runs() {
// Create two extractors with same parameters
let mut extractor1 = MLFeatureExtractor::new(50);
let mut extractor2 = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Feed identical data to both
for i in 0..60 {
let price = 4500.0 + (i as f64 * 0.5);
let volume = 100_000.0;
let features1 = extractor1.extract_features(price, volume, timestamp);
let features2 = extractor2.extract_features(price, volume, timestamp);
if i >= 50 && features1.len() >= 22 && features2.len() >= 22 {
let macd1 = features1[20];
let signal1 = features1[21];
let macd2 = features2[20];
let signal2 = features2[21];
// MACD should be deterministic (identical across runs)
assert!(
(macd1 - macd2).abs() < 1e-10,
"MACD differs: {:.15} vs {:.15} at bar {}",
macd1,
macd2,
i
);
assert!(
(signal1 - signal2).abs() < 1e-10,
"MACD Signal differs: {:.15} vs {:.15} at bar {}",
signal1,
signal2,
i
);
}
}
}
#[test]
fn test_macd_ema_periods_correctness() {
// Validate MACD uses correct EMA periods (12, 26, 9)
let mut extractor = MLFeatureExtractor::new(50);
let timestamp = Utc::now();
// Build steady uptrend
for i in 0..60 {
let price = 4500.0 + (i as f64 * 1.0);
let features = extractor.extract_features(price, 100_000.0, timestamp);
if i >= 50 && features.len() >= 26 {
let macd = features[24];
let signal = features[25];
// During steady uptrend:
// - EMA12 rises faster than EMA26 (shorter period = more responsive)
// - MACD (EMA12 - EMA26) should be positive and increasing
// - Signal (EMA9 of MACD) should lag behind MACD
println!(
"Bar {}: Price={:.2}, MACD={:.6}, Signal={:.6}",
i,
4500.0 + (i as f64),
macd,
signal
);
assert!(
macd.is_finite() && signal.is_finite(),
"MACD values should be finite during steady uptrend"
);
}
}
}
// Helper function for volatility calculation
fn calculate_volatility(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mean: f64 = values.iter().sum::<f64>() / values.len() as f64;
let variance: f64 =
values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
variance.sqrt()
}