Files
foxhunt/services/trading_service/tests/paper_trading_ml_integration_test.rs
jgrusewski dd62f3fcfd refactor: eliminate candle from entire workspace — tests, examples, Cargo.toml
Final cleanup:
- 61 test files + 5 example files: candle imports replaced
- 8 testing/integration files: migrated to cudarc/ml-core types
- 3 services/trading_service test files: migrated
- Root Cargo.toml: candle-core, candle-nn removed from [workspace.dependencies]
- crates/ml/Cargo.toml: candle-nn dependency removed
- testing/e2e/Cargo.toml: candle-core dependency removed

Zero active candle_core/candle_nn/candle_optimisers code references remain.
Zero candle dependency declarations in any Cargo.toml.
Remaining "candle" strings are exclusively in doc comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:53:47 +01:00

559 lines
17 KiB
Rust

#![allow(unexpected_cfgs)]
#![cfg(feature = "__trading_service_integration")]
//! TDD Integration Tests for Paper Trading ML Integration
//!
//! Mission: Integrate ML predictions with paper trading executor using strict TDD methodology
//! Phase: RED (Failing Tests) → GREEN (Minimal Implementation) → REFACTOR (Quality)
//!
//! Tests cover:
//! - Paper trading with ML signals
//! - ML signal to order conversion
//! - Position sizing based on confidence
//! - ML prediction tracking in PostgreSQL
//! - Risk limits override ML signals
//! - Fallback to rule-based on ML failure
//! - Performance feedback loop
use ml_core::native_types::NativeDevice;
use common::{CommonError, OrderSide, OrderType};
use sqlx::PgPool;
use std::path::PathBuf;
use uuid::Uuid;
// ============================================================================
// Helper Functions (Test Infrastructure)
// ============================================================================
/// Create test database pool
async fn get_test_db_pool() -> PgPool {
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
"postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string()
});
PgPool::connect(&database_url)
.await
.expect("Failed to connect to test database")
}
/// Create test ensemble coordinator with 3 models (DQN, PPO, MAMBA2)
fn create_test_ensemble() -> std::sync::Arc<trading_service::EnsembleCoordinator> {
use std::sync::Arc;
let coordinator = Arc::new(trading_service::EnsembleCoordinator::new());
// Note: In real usage, models would be loaded and registered with the coordinator
// For tests, we create a minimal ensemble coordinator without loaded models
coordinator
}
/// Create test paper trading executor with ML
async fn create_test_executor_with_ml(pool: PgPool) -> trading_service::PaperTradingExecutor {
let ensemble = create_test_ensemble();
trading_service::PaperTradingExecutor::new_with_ml(pool, ensemble)
.await
.expect("Failed to create executor with ML")
}
/// Load test OHLCV data (50 bars)
fn load_test_ohlcv_data(_symbol: &str, num_bars: usize) -> Vec<(f64, f64, f64, f64, f64)> {
// Generate synthetic OHLCV data with realistic pattern
let mut data = Vec::new();
let mut base_price = 4500.0; // ES.FUT starting price
for i in 0..num_bars {
let trend = (i as f64 * 0.1).sin(); // Add sine wave trend
let open = base_price + trend * 10.0;
let high = open + (i as f64 % 5.0) + 5.0;
let low = open - (i as f64 % 3.0) - 3.0;
let close = open + trend * 5.0;
let volume = 1000.0 + (i as f64 * 10.0);
data.push((open, high, low, close, volume));
base_price = close; // Next bar starts from previous close
}
data
}
/// Trading signal structure
#[derive(Debug, Clone)]
struct TradingSignal {
action: Option<Action>,
confidence: f64,
source: SignalSource,
model_votes: Option<Vec<(String, usize, f32)>>,
}
/// Action enum
#[derive(Debug, Clone, Copy, PartialEq)]
enum Action {
Buy,
Sell,
Hold,
}
/// Signal source
#[derive(Debug, Clone, Copy, PartialEq)]
enum SignalSource {
ML,
RuleBased,
}
/// Order structure for testing
#[derive(Debug, Clone)]
struct Order {
id: Uuid,
symbol: String,
side: OrderSide,
quantity: i32,
order_type: OrderType,
price: Option<i64>,
}
// ============================================================================
// TEST 1: Paper Trading with ML Signals (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_paper_trading_with_ml_signals() {
// Arrange: Create executor with ML
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool).await;
// Generate market data
let market_data = load_test_ohlcv_data("ES.FUT", 50);
// Act: Generate ML signal
let result = executor.generate_ml_signal(&market_data).await;
// Assert: Should generate valid ML signal
assert!(
result.is_ok(),
"ML signal generation failed: {:?}",
result.err()
);
let signal = result.unwrap();
assert!(signal.action.is_some(), "ML signal should have an action");
assert_eq!(signal.source, SignalSource::ML, "Source should be ML");
assert!(
signal.confidence >= 0.0 && signal.confidence <= 1.0,
"Confidence should be in [0, 1], got {}",
signal.confidence
);
}
// ============================================================================
// TEST 2: ML Signal to Order Conversion (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_ml_signal_to_order_conversion() {
// Arrange: Create executor and signal
let pool = get_test_db_pool().await;
let executor = create_test_executor_with_ml(pool).await;
let signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.85,
source: SignalSource::ML,
model_votes: Some(vec![
("DQN".to_string(), 0, 0.9),
("PPO".to_string(), 0, 0.8),
]),
};
// Act: Convert signal to order
let result = executor.convert_signal_to_order(&signal, "ES.FUT").await;
// Assert: Order should be created correctly
assert!(
result.is_ok(),
"Signal to order conversion failed: {:?}",
result.err()
);
let order = result.unwrap();
assert_eq!(order.side, OrderSide::Buy, "Order side should be Buy");
assert!(order.quantity > 0, "Quantity should be positive");
assert_eq!(
order.order_type,
OrderType::Market,
"Should be market order"
);
}
// ============================================================================
// TEST 3: Position Sizing Based on Confidence (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_position_sizing_based_on_confidence() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let executor = create_test_executor_with_ml(pool).await;
// High confidence signal (0.9)
let high_conf_signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.9,
source: SignalSource::ML,
model_votes: None,
};
// Low confidence signal (0.6)
let low_conf_signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.6,
source: SignalSource::ML,
model_votes: None,
};
// Act: Convert both to orders
let high_conf_order = executor
.convert_signal_to_order(&high_conf_signal, "ES.FUT")
.await
.expect("High confidence order failed");
let low_conf_order = executor
.convert_signal_to_order(&low_conf_signal, "ES.FUT")
.await
.expect("Low confidence order failed");
// Assert: Higher confidence should result in larger position
assert!(
high_conf_order.quantity > low_conf_order.quantity,
"High confidence ({}) should have larger position than low confidence ({})",
high_conf_order.quantity,
low_conf_order.quantity
);
}
// ============================================================================
// TEST 4: ML Prediction Tracking in PostgreSQL (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_ml_prediction_tracking() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool.clone()).await;
let signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.85,
source: SignalSource::ML,
model_votes: None,
};
// Act: Execute ML signal (should track prediction)
let result = executor.execute_ml_signal(&signal, "ES.FUT").await;
assert!(
result.is_ok(),
"ML signal execution failed: {:?}",
result.err()
);
let order = result.unwrap();
// Assert: Prediction should be stored in ml_predictions table
let prediction = sqlx::query!(
r#"
SELECT id, predicted_action, confidence, symbol
FROM ml_predictions
WHERE order_id = $1
ORDER BY prediction_timestamp DESC
LIMIT 1
"#,
order.id
)
.fetch_optional(&pool)
.await
.expect("Failed to query predictions");
assert!(
prediction.is_some(),
"Prediction should be stored in database"
);
let pred = prediction.unwrap();
assert_eq!(
pred.predicted_action, 0,
"Predicted action should be 0 (Buy)"
);
assert!(
(pred.confidence - 0.85).abs() < 0.01,
"Confidence should be 0.85"
);
assert_eq!(pred.symbol, "ES.FUT", "Symbol should be ES.FUT");
}
// ============================================================================
// TEST 5: Risk Limits Override ML Signals (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_risk_limits_override_ml_signals() {
// Arrange: Create executor and set position limit
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool).await;
// Simulate position limit reached
executor
.set_position_limit("ES.FUT", 0)
.await
.expect("Failed to set position limit");
let signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.95, // High confidence, but should be rejected
source: SignalSource::ML,
model_votes: None,
};
// Act: Try to execute ML signal
let result = executor.execute_ml_signal(&signal, "ES.FUT").await;
// Assert: Should reject due to position limit
assert!(result.is_err(), "Should reject when position limit reached");
let error = result.unwrap_err();
let error_msg = error.to_string();
assert!(
error_msg.contains("Position limit") || error_msg.contains("position"),
"Error should mention position limit, got: {}",
error_msg
);
}
// ============================================================================
// TEST 6: Fallback to Rule-Based on ML Failure (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_fallback_to_rule_based_on_ml_failure() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool).await;
// Disable ML to simulate failure
executor.disable_ml().await;
let market_data = load_test_ohlcv_data("ES.FUT", 50);
// Act: Generate signal (should fallback to rule-based)
let result = executor.generate_signal(&market_data).await;
// Assert: Should fallback successfully
assert!(
result.is_ok(),
"Fallback to rule-based failed: {:?}",
result.err()
);
let signal = result.unwrap();
assert_eq!(
signal.source,
SignalSource::RuleBased,
"Should fallback to rule-based"
);
assert!(
signal.action.is_some(),
"Rule-based should still generate signal"
);
}
// ============================================================================
// TEST 7: ML Performance Feedback Loop (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_ml_performance_feedback_loop() {
// Arrange: Create executor and execute signal
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool.clone()).await;
let signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.85,
source: SignalSource::ML,
model_votes: None,
};
let order = executor
.execute_ml_signal(&signal, "ES.FUT")
.await
.expect("ML signal execution failed");
// Act: Record outcome (simulate profitable trade)
let result = executor.record_outcome(order.id, 100.0).await;
// Assert: Outcome should be recorded
assert!(
result.is_ok(),
"Recording outcome failed: {:?}",
result.err()
);
// Verify outcome in database
let prediction = sqlx::query!(
r#"
SELECT actual_action, pnl, outcome_recorded_at
FROM ml_predictions
WHERE order_id = $1
"#,
order.id
)
.fetch_one(&pool)
.await
.expect("Failed to fetch prediction");
assert!(
prediction.actual_action.is_some(),
"Actual action should be recorded"
);
assert!(prediction.pnl.is_some(), "PnL should be recorded");
assert!(
prediction.outcome_recorded_at.is_some(),
"Outcome timestamp should be set"
);
assert!(
(prediction.pnl.unwrap() - 100.0).abs() < 0.01,
"PnL should be $100"
);
}
// ============================================================================
// TEST 8: Confidence Threshold Filtering (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_confidence_threshold_filtering() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let executor = create_test_executor_with_ml(pool).await;
// Very low confidence signal (below trading threshold)
let low_conf_signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.4, // Below 0.6 threshold
source: SignalSource::ML,
model_votes: None,
};
// Act: Try to convert to order
let result = executor
.convert_signal_to_order(&low_conf_signal, "ES.FUT")
.await;
// Assert: Should reject low confidence signals
assert!(result.is_err(), "Should reject low confidence signals");
let error = result.unwrap_err();
assert!(
error.to_string().contains("Confidence too low")
|| error.to_string().contains("confidence"),
"Error should mention confidence threshold"
);
}
// ============================================================================
// TEST 9: Multi-Symbol ML Trading (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_multi_symbol_ml_trading() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool).await;
let symbols = vec!["ES.FUT", "NQ.FUT", "ZN.FUT"];
// Act: Execute ML signals for multiple symbols
for symbol in &symbols {
let signal = TradingSignal {
action: Some(Action::Buy),
confidence: 0.8,
source: SignalSource::ML,
model_votes: None,
};
let result = executor.execute_ml_signal(&signal, symbol).await;
assert!(result.is_ok(), "ML signal for {} failed", symbol);
}
// Assert: All symbols should have executed trades
let position_summary = executor.get_position_summary().await;
for symbol in &symbols {
assert!(
position_summary.contains_key(*symbol),
"Position should exist for {}",
symbol
);
}
}
// ============================================================================
// TEST 10: Ensemble Agreement Weighting (RED)
// ============================================================================
#[tokio::test]
#[ignore = "RED phase - will fail until implementation exists"]
async fn test_ensemble_agreement_weighting() {
// Arrange: Create executor
let pool = get_test_db_pool().await;
let mut executor = create_test_executor_with_ml(pool).await;
let market_data = load_test_ohlcv_data("ES.FUT", 50);
// Act: Generate signal with ensemble voting
let result = executor.generate_ml_signal(&market_data).await;
assert!(result.is_ok());
let signal = result.unwrap();
// Assert: Should have model votes with agreement weighting
assert!(signal.model_votes.is_some(), "Should have model votes");
let votes = signal.model_votes.unwrap();
// Check that confidence reflects ensemble agreement
let agreement_ratio = calculate_agreement_ratio(&votes);
// If all models agree, confidence should be high
if agreement_ratio > 0.8 {
assert!(
signal.confidence > 0.7,
"High agreement should result in high confidence, got {}",
signal.confidence
);
}
}
/// Helper: Calculate agreement ratio from model votes
fn calculate_agreement_ratio(votes: &[(String, usize, f32)]) -> f64 {
if votes.is_empty() {
return 0.0;
}
// Count votes for most common action
let mut action_counts = std::collections::HashMap::new();
for (_, action, _) in votes {
*action_counts.entry(action).or_insert(0) += 1;
}
let max_count = action_counts.values().max().copied().unwrap_or(0);
max_count as f64 / votes.len() as f64
}