Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
5.4 KiB
Rust
179 lines
5.4 KiB
Rust
//! Ensemble integration test -- all 4 models contributing to a single trading decision
|
|
//!
|
|
//! Verifies: DQN + PPO + TFT + Mamba2 -> EnsembleCoordinator -> EnsembleDecision
|
|
//! This is the critical test that proves the ensemble pipeline works end-to-end.
|
|
|
|
use ml::dqn::dqn::DQNConfig;
|
|
use ml::ensemble::adapters::{
|
|
DqnInferenceAdapter, Mamba2InferenceAdapter, PpoInferenceAdapter, TftInferenceAdapter,
|
|
};
|
|
use ml::ensemble::coordinator::EnsembleCoordinator;
|
|
use ml::ensemble::inference_adapter::{FeatureVector, ModelInferenceAdapter};
|
|
use ml::ensemble::TradingAction;
|
|
use ml::mamba::Mamba2Config;
|
|
use ml::ppo::ppo::PPOConfig;
|
|
use ml::tft::TFTConfig;
|
|
use ml::Features;
|
|
|
|
fn small_dqn_config() -> DQNConfig {
|
|
DQNConfig {
|
|
state_dim: 51,
|
|
num_actions: 45,
|
|
hidden_dims: vec![32, 32],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn small_ppo_config() -> PPOConfig {
|
|
PPOConfig {
|
|
state_dim: 64,
|
|
num_actions: 45,
|
|
policy_hidden_dims: vec![32, 32],
|
|
value_hidden_dims: vec![32, 32],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn small_tft_config() -> TFTConfig {
|
|
TFTConfig {
|
|
input_dim: 20,
|
|
hidden_dim: 32,
|
|
num_heads: 2,
|
|
num_layers: 1,
|
|
prediction_horizon: 5,
|
|
sequence_length: 4,
|
|
num_quantiles: 9,
|
|
num_static_features: 6,
|
|
num_known_features: 6,
|
|
num_unknown_features: 8,
|
|
dropout_rate: 0.0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn small_mamba2_config() -> Mamba2Config {
|
|
Mamba2Config {
|
|
d_model: 32,
|
|
d_state: 8,
|
|
d_head: 8,
|
|
num_heads: 2,
|
|
expand: 2,
|
|
num_layers: 1,
|
|
max_seq_len: 8,
|
|
dropout: 0.0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
const TFT_SEQ_LEN: usize = 4;
|
|
const MAMBA2_SEQ_LEN: usize = 4;
|
|
|
|
#[tokio::test]
|
|
async fn test_ensemble_all_4_models_produce_decision() {
|
|
// Create all 4 adapters
|
|
let dqn = DqnInferenceAdapter::new(small_dqn_config()).expect("DQN adapter creation failed");
|
|
let ppo = PpoInferenceAdapter::new(small_ppo_config()).expect("PPO adapter creation failed");
|
|
let tft =
|
|
TftInferenceAdapter::new(small_tft_config(), TFT_SEQ_LEN).expect("TFT adapter creation failed");
|
|
let mamba2 = Mamba2InferenceAdapter::new(small_mamba2_config(), MAMBA2_SEQ_LEN)
|
|
.expect("Mamba2 adapter creation failed");
|
|
|
|
// Warm up TFT and Mamba2 sequence buffers
|
|
for i in 0..TFT_SEQ_LEN {
|
|
let fv = FeatureVector {
|
|
values: vec![0.1; 51],
|
|
timestamp: 1_700_000_000 + i as i64,
|
|
};
|
|
let _ = tft.predict(&fv);
|
|
let _ = mamba2.predict(&fv);
|
|
}
|
|
|
|
// Verify all adapters are ready
|
|
assert!(dqn.is_ready(), "DQN should be ready");
|
|
assert!(ppo.is_ready(), "PPO should be ready");
|
|
assert!(tft.is_ready(), "TFT should be ready after warmup");
|
|
assert!(mamba2.is_ready(), "Mamba2 should be ready after warmup");
|
|
|
|
// Build ensemble coordinator
|
|
let mut coordinator = EnsembleCoordinator::new();
|
|
coordinator.add_adapter(Box::new(dqn));
|
|
coordinator.add_adapter(Box::new(ppo));
|
|
coordinator.add_adapter(Box::new(tft));
|
|
coordinator.add_adapter(Box::new(mamba2));
|
|
|
|
// Register models with equal weights
|
|
coordinator
|
|
.register_model("DQN".to_string(), 0.25)
|
|
.await
|
|
.expect("register DQN");
|
|
coordinator
|
|
.register_model("PPO".to_string(), 0.25)
|
|
.await
|
|
.expect("register PPO");
|
|
coordinator
|
|
.register_model("TFT".to_string(), 0.25)
|
|
.await
|
|
.expect("register TFT");
|
|
coordinator
|
|
.register_model("MAMBA-2".to_string(), 0.25)
|
|
.await
|
|
.expect("register MAMBA-2");
|
|
|
|
// Create Features for ensemble prediction
|
|
let features = Features::new(
|
|
vec![0.1; 51],
|
|
(0..51).map(|i| format!("f{}", i)).collect(),
|
|
);
|
|
|
|
// Get ensemble decision
|
|
let decision = coordinator
|
|
.predict(&features)
|
|
.await
|
|
.expect("Ensemble prediction should succeed");
|
|
|
|
// Verify decision is valid
|
|
assert!(
|
|
decision.confidence >= 0.0 && decision.confidence <= 1.0,
|
|
"ensemble confidence {} out of [0,1]",
|
|
decision.confidence
|
|
);
|
|
assert!(
|
|
decision.signal >= -1.0 && decision.signal <= 1.0,
|
|
"ensemble signal {} out of [-1,1]",
|
|
decision.signal
|
|
);
|
|
assert!(decision.signal.is_finite(), "signal must not be NaN/Inf");
|
|
assert!(
|
|
decision.confidence.is_finite(),
|
|
"confidence must not be NaN/Inf"
|
|
);
|
|
|
|
// Verify we got votes from all 4 models
|
|
assert!(!decision.model_votes.is_empty(), "Should have model votes");
|
|
|
|
// Action should be Buy, Sell, or Hold
|
|
match decision.action {
|
|
TradingAction::Buy | TradingAction::Sell | TradingAction::Hold => {} // Valid
|
|
}
|
|
|
|
println!(
|
|
"Ensemble decision: action={:?}, signal={:.4}, confidence={:.4}, disagreement={:.4}",
|
|
decision.action, decision.signal, decision.confidence, decision.disagreement_rate
|
|
);
|
|
for (name, vote) in &decision.model_votes {
|
|
println!(
|
|
" {} -> signal={:.4}, confidence={:.4}",
|
|
name, vote.signal, vote.confidence
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_ensemble_no_adapters_returns_error() {
|
|
let coordinator = EnsembleCoordinator::new();
|
|
let features = Features::new(vec![0.1; 51], vec!["f0".to_string()]);
|
|
|
|
let result = coordinator.predict(&features).await;
|
|
assert!(result.is_err(), "Empty ensemble should return error");
|
|
}
|