diff --git a/ml/tests/paper_trading_integration_test.rs b/ml/tests/paper_trading_integration_test.rs new file mode 100644 index 000000000..ad21020ed --- /dev/null +++ b/ml/tests/paper_trading_integration_test.rs @@ -0,0 +1,142 @@ +//! Paper trading pipeline integration test. +//! +//! Replays synthetic price data through the full pipeline: +//! Feature extraction -> Ensemble -> TradeSignal -> PaperBroker -> PnL + +#![allow(unused_crate_dependencies)] +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::panic)] +#![allow(clippy::indexing_slicing)] + +use ml::dqn::dqn::DQNConfig; +use ml::ensemble::adapters::dqn::DqnInferenceAdapter; +use ml::ensemble::adapters::ppo::PpoInferenceAdapter; +use ml::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter}; +use ml::ensemble::inference_ensemble::InferenceEnsemble; +use ml::ensemble::signal::TradeSignal; +use ml::paper_trading::broker::PaperBroker; +use ml::paper_trading::pnl_tracker::PnLTracker; +use ml::ppo::ppo::PPOConfig; + +#[test] +fn test_paper_trading_pipeline_synthetic_data() { + // 1. Create DQN adapter with small config + let dqn_config = DQNConfig { + state_dim: 51, + num_actions: 3, // buy / hold / sell + hidden_dims: vec![32, 32], + ..Default::default() + }; + let dqn_adapter = + DqnInferenceAdapter::new(dqn_config).expect("DQN adapter should initialise"); + assert!(dqn_adapter.is_ready(), "DQN adapter must be ready"); + + // 2. Create PPO adapter with matching config + let ppo_config = PPOConfig { + state_dim: 51, + num_actions: 3, + policy_hidden_dims: vec![32, 32], + value_hidden_dims: vec![32, 32], + ..Default::default() + }; + let ppo_adapter = + PpoInferenceAdapter::new(ppo_config).expect("PPO adapter should initialise"); + assert!(ppo_adapter.is_ready(), "PPO adapter must be ready"); + + // 3. Build InferenceEnsemble with both adapters + let adapters: Vec> = + vec![Box::new(dqn_adapter), Box::new(ppo_adapter)]; + let ensemble = InferenceEnsemble::new(adapters); + assert_eq!(ensemble.ready_count(), 2, "Both models must be ready"); + + // 4. Create PaperBroker with $100k cash and 1bps slippage + let mut broker = PaperBroker::new(100_000.0, 0.0001); + + // 5. Create PnLTracker with 252-bar window + let mut pnl_tracker = PnLTracker::new(252); + + // 6. Simulate 100 bars of synthetic price data + let mut price = 1.0850_f64; + let num_bars = 100_usize; + + for i in 0..num_bars { + // Create synthetic feature vector (51-dim) + let values: Vec = (0..51) + .map(|j| { + // Deterministic pseudo-features based on price and bar index + let base = price * (1.0 + 0.001 * ((j as f64) - 25.0)); + base * (1.0 + 0.0001 * (i as f64)) + }) + .collect(); + + let fv = FeatureVector { + values, + timestamp: 1_700_000_000_000_000 + (i as i64 * 60_000_000), // 1-min bars + }; + + // Get ensemble prediction + let prediction = ensemble + .predict(&fv) + .expect("Ensemble prediction should succeed"); + + // Convert to trade signal + let signal = TradeSignal::from_prediction(&prediction, "6E.FUT"); + + // Execute through broker + let _fill = broker.execute_signal(&signal, price); + + // Record equity + let equity = broker.equity(price); + pnl_tracker.record_equity(equity); + + // Random-walk the price (deterministic using bar index) + price += 0.0001 * ((i % 7) as f64 - 3.0); + } + + // 7. Assertions + let sharpe = pnl_tracker.rolling_sharpe(); + let drawdown = pnl_tracker.max_drawdown(); + let num_trades = broker.num_trades(); + let num_obs = pnl_tracker.num_observations(); + let cum_return = pnl_tracker.cumulative_return(); + + println!("=== Paper Trading Pipeline Summary ==="); + println!(" Bars simulated: {}", num_bars); + println!(" Trades executed: {}", num_trades); + println!(" Rolling Sharpe: {:.4}", sharpe); + println!(" Max drawdown: {:.4}", drawdown); + println!(" Cumulative return: {:.6}", cum_return); + println!(" Final equity: {:.2}", broker.equity(price)); + println!(" Final price: {:.4}", price); + println!(" Observations: {}", num_obs); + println!("======================================"); + + // Sharpe ratio should be finite (not NaN/Inf) + assert!( + sharpe.is_finite(), + "Sharpe ratio must be finite, got {}", + sharpe + ); + + // Drawdown should be finite and bounded [0, 1] + assert!( + drawdown.is_finite() && drawdown <= 1.0, + "Drawdown must be finite and <= 1.0, got {}", + drawdown + ); + + // At least some trades should have been executed + assert!( + num_trades > 0, + "Expected at least 1 trade, got {}", + num_trades + ); + + // PnL tracker should have recorded all 100 observations + assert!( + num_obs >= num_bars, + "Expected >= {} observations, got {}", + num_bars, num_obs + ); +}