diff --git a/crates/backtesting/src/model_loader.rs b/crates/backtesting/src/model_loader.rs index 561924ac1..92631846b 100644 --- a/crates/backtesting/src/model_loader.rs +++ b/crates/backtesting/src/model_loader.rs @@ -161,7 +161,7 @@ pub async fn load_models_for_backtest(specs: &[ModelSpec]) -> Result<(), MLError "DQN" => { let config = DQNConfig { state_dim: 51, - num_actions: 45, + num_actions: 63, hidden_dims: vec![128, 128], ..Default::default() }; @@ -177,7 +177,7 @@ pub async fn load_models_for_backtest(specs: &[ModelSpec]) -> Result<(), MLError "PPO" => { let config = PPOConfig { state_dim: 51, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![128, 64], value_hidden_dims: vec![128, 64], ..Default::default() @@ -217,7 +217,7 @@ mod tests { fn test_dqn_config() -> DQNConfig { DQNConfig { state_dim: 51, - num_actions: 45, + num_actions: 63, hidden_dims: vec![64, 64], ..Default::default() } diff --git a/crates/backtesting/tests/dbn_backtest_integration.rs b/crates/backtesting/tests/dbn_backtest_integration.rs index 87abc8539..8618a197a 100644 --- a/crates/backtesting/tests/dbn_backtest_integration.rs +++ b/crates/backtesting/tests/dbn_backtest_integration.rs @@ -171,7 +171,7 @@ async fn test_replay_engine_streams_events() { async fn test_model_loads_and_predicts() { let config = DQNConfig { state_dim: 51, - num_actions: 45, + num_actions: 63, hidden_dims: vec![64, 64], ..Default::default() }; diff --git a/crates/ml-core/src/action_space.rs b/crates/ml-core/src/action_space.rs index 239e46680..a8134d4bd 100644 --- a/crates/ml-core/src/action_space.rs +++ b/crates/ml-core/src/action_space.rs @@ -3,18 +3,18 @@ pub use crate::common::action::{ExposureLevel, FactoredAction, OrderType, Urgenc /// Returns action mask where true=valid, false=invalid based on position limits. /// /// Prevents invalid exposure actions that would violate position limits. -/// DQN outputs 5 exposure actions (Short100, Short50, Flat, Long50, Long100). +/// DQN outputs 7 exposure actions (ShortSmall..LongFull). /// /// # Arguments /// * `_current_position` - Current portfolio position (unused - actions use absolute targets) /// * `max_position` - Maximum allowed position magnitude (typically 2.0) /// /// # Returns -/// Boolean mask of length 5 where: +/// Boolean mask of length 7 where: /// - `true` = exposure action is valid (does not violate position limits) /// - `false` = exposure action is invalid (would exceed max_position) pub fn get_valid_action_mask(_current_position: f64, max_position: f64) -> Vec { - let exposures = [-1.0_f64, -0.5, 0.0, 0.5, 1.0]; // Short100..Long100 + let exposures = [-0.25_f64, -0.50, -1.0, 0.0, 0.25, 0.50, 1.0]; exposures .iter() .map(|&exp| exp.abs() <= max_position) @@ -28,15 +28,13 @@ mod tests { #[test] fn test_exposure_enum_values() { - assert_eq!(ExposureLevel::Short100 as usize, 0); - assert_eq!(ExposureLevel::Short75 as usize, 1); - assert_eq!(ExposureLevel::Short50 as usize, 2); - assert_eq!(ExposureLevel::Short25 as usize, 3); - assert_eq!(ExposureLevel::Flat as usize, 4); - assert_eq!(ExposureLevel::Long25 as usize, 5); - assert_eq!(ExposureLevel::Long50 as usize, 6); - assert_eq!(ExposureLevel::Long75 as usize, 7); - assert_eq!(ExposureLevel::Long100 as usize, 8); + assert_eq!(ExposureLevel::ShortSmall as usize, 0); + assert_eq!(ExposureLevel::ShortHalf as usize, 1); + assert_eq!(ExposureLevel::ShortFull as usize, 2); + assert_eq!(ExposureLevel::Flat as usize, 3); + assert_eq!(ExposureLevel::LongSmall as usize, 4); + assert_eq!(ExposureLevel::LongHalf as usize, 5); + assert_eq!(ExposureLevel::LongFull as usize, 6); } #[test] @@ -55,8 +53,8 @@ mod tests { #[test] fn test_action_from_index_bidirectional() { - // Test all 45 actions round-trip - for idx in 0..45 { + // Test all 63 actions round-trip + for idx in 0..63 { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.to_index(), @@ -70,25 +68,20 @@ mod tests { #[test] fn test_action_from_index_bounds() { // Test out of bounds indices - assert!(FactoredAction::from_index(81).is_err()); + assert!(FactoredAction::from_index(63).is_err()); assert!(FactoredAction::from_index(100).is_err()); assert!(FactoredAction::from_index(usize::MAX).is_err()); } #[test] fn test_target_exposure_values() { - // 4-branch: dir × mag. Index = dir*3 + mag. - // dir: 0=Short(-1), 1=Flat(0), 2=Long(+1) - // mag: 0=Small(0.25), 1=Half(0.50), 2=Full(1.00) - assert_eq!(ExposureLevel::Short100.target_exposure(), -0.25); // Short×Small - assert_eq!(ExposureLevel::Short75.target_exposure(), -0.50); // Short×Half - assert_eq!(ExposureLevel::Short50.target_exposure(), -1.0); // Short×Full - assert_eq!(ExposureLevel::Short25.target_exposure(), 0.0); // Flat×Small - assert_eq!(ExposureLevel::Flat.target_exposure(), 0.0); // Flat×Half - assert_eq!(ExposureLevel::Long25.target_exposure(), 0.0); // Flat×Full - assert_eq!(ExposureLevel::Long50.target_exposure(), 0.25); // Long×Small - assert_eq!(ExposureLevel::Long75.target_exposure(), 0.50); // Long×Half - assert_eq!(ExposureLevel::Long100.target_exposure(), 1.0); // Long×Full + assert_eq!(ExposureLevel::ShortSmall.target_exposure(), -0.25); + assert_eq!(ExposureLevel::ShortHalf.target_exposure(), -0.50); + assert_eq!(ExposureLevel::ShortFull.target_exposure(), -1.0); + assert_eq!(ExposureLevel::Flat.target_exposure(), 0.0); + assert_eq!(ExposureLevel::LongSmall.target_exposure(), 0.25); + assert_eq!(ExposureLevel::LongHalf.target_exposure(), 0.50); + assert_eq!(ExposureLevel::LongFull.target_exposure(), 1.0); } #[test] @@ -109,12 +102,12 @@ mod tests { #[test] fn test_action_equality() { let action1 = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); let action2 = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); @@ -127,35 +120,35 @@ mod tests { #[test] fn test_action_debug() { let action = FactoredAction::new( - ExposureLevel::Long50, + ExposureLevel::LongHalf, OrderType::LimitMaker, Urgency::Patient, ); let debug_str = format!("{:?}", action); - assert!(debug_str.contains("Long50")); + assert!(debug_str.contains("LongHalf")); assert!(debug_str.contains("LimitMaker")); assert!(debug_str.contains("Patient")); } #[test] fn test_action_clone() { - let action1 = FactoredAction::new(ExposureLevel::Short50, OrderType::IoC, Urgency::Normal); + let action1 = FactoredAction::new(ExposureLevel::ShortFull, OrderType::IoC, Urgency::Normal); let action2 = action1.clone(); assert_eq!(action1, action2); } #[test] fn test_index_bijection() { - // Verify all 45 indices map to unique actions + // Verify all 63 indices map to unique actions use std::collections::HashSet; let mut actions = HashSet::new(); - for idx in 0..45 { + for idx in 0..63 { let action = FactoredAction::from_index(idx).unwrap(); assert!(actions.insert(action), "Duplicate action for index {}", idx); } - assert_eq!(actions.len(), 45); + assert_eq!(actions.len(), 63); } #[test] @@ -163,23 +156,22 @@ mod tests { // Test common neutral action let action = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); - // Index = 4 * 9 + 0 * 3 + 1 = 36 + 0 + 1 = 37 - assert_eq!(action.to_index(), 37); + // Index = 3 * 9 + 0 * 3 + 1 = 27 + 0 + 1 = 28 + assert_eq!(action.to_index(), 28); assert_eq!(action.target_exposure(), 0.0); assert_eq!(action.transaction_cost(), 0.0015); // Wave 2.5 calibration assert_eq!(action.urgency_weight(), 1.0); // Round-trip - let reconstructed = FactoredAction::from_index(37).unwrap(); + let reconstructed = FactoredAction::from_index(28).unwrap(); assert_eq!(action, reconstructed); } #[test] fn test_extreme_actions() { - // 4-branch: Short100(=0) is Short×Small = -0.25 (NOT -1.0) - // Full short is Short50(=2) = Short×Full = -1.0 + // ShortFull = -1.0 let short_extreme = FactoredAction::new( - ExposureLevel::Short50, // Short×Full = -1.0 + ExposureLevel::ShortFull, OrderType::Market, Urgency::Aggressive, ); @@ -188,13 +180,13 @@ mod tests { assert_eq!(short_extreme.transaction_cost(), 0.0015); assert_eq!(short_extreme.urgency_weight(), 1.5); - // Long100(=8) is Long×Full = +1.0 (still correct) + // LongFull = +1.0 let long_extreme = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); - assert_eq!(long_extreme.to_index(), 8 * 9 + 0 * 3 + 2); // 74 + assert_eq!(long_extreme.to_index(), 6 * 9 + 0 * 3 + 2); // 56 assert_eq!(long_extreme.target_exposure(), 1.0); assert_eq!(long_extreme.transaction_cost(), 0.0015); assert_eq!(long_extreme.urgency_weight(), 1.5); @@ -203,7 +195,7 @@ mod tests { #[test] fn test_serialization() { let action = FactoredAction::new( - ExposureLevel::Long50, + ExposureLevel::LongHalf, OrderType::LimitMaker, Urgency::Patient, ); @@ -219,12 +211,12 @@ mod tests { #[test] fn test_action_masking_all_valid_at_standard_limit() { - // With max_position=2.0, all 5 exposure actions should be valid + // With max_position=2.0, all 7 exposure actions should be valid let mask = get_valid_action_mask(0.0, 2.0); - assert_eq!(mask.len(), 5); + assert_eq!(mask.len(), 7); assert!( mask.iter().all(|&v| v), - "All 5 exposure actions should be valid at max_position=2.0" + "All 7 exposure actions should be valid at max_position=2.0" ); } @@ -232,27 +224,29 @@ mod tests { fn test_action_masking_at_limit_1() { // With max_position=1.0, all actions valid (max exposure = 1.0) let mask = get_valid_action_mask(0.0, 1.0); - assert_eq!(mask.len(), 5); + assert_eq!(mask.len(), 7); assert!(mask.iter().all(|&v| v), "All valid at max_position=1.0"); } #[test] fn test_action_masking_restrictive_limit() { - // With max_position=0.6, Short100/Long100 masked (exposure ±1.0 > 0.6) + // With max_position=0.6, ShortFull/LongFull masked (exposure |1.0| > 0.6) let mask = get_valid_action_mask(0.0, 0.6); - assert_eq!(mask.len(), 5); - assert!(!mask[0], "Short100 should be INVALID at max_position=0.6"); - assert!(mask[1], "Short50 should be valid at max_position=0.6"); - assert!(mask[2], "Flat should be valid at max_position=0.6"); - assert!(mask[3], "Long50 should be valid at max_position=0.6"); - assert!(!mask[4], "Long100 should be INVALID at max_position=0.6"); + assert_eq!(mask.len(), 7); + assert!(mask[0], "ShortSmall (0.25) should be valid at max_position=0.6"); + assert!(mask[1], "ShortHalf (0.50) should be valid at max_position=0.6"); + assert!(!mask[2], "ShortFull (1.0) should be INVALID at max_position=0.6"); + assert!(mask[3], "Flat should be valid at max_position=0.6"); + assert!(mask[4], "LongSmall (0.25) should be valid at max_position=0.6"); + assert!(mask[5], "LongHalf (0.50) should be valid at max_position=0.6"); + assert!(!mask[6], "LongFull (1.0) should be INVALID at max_position=0.6"); } #[test] fn test_action_masking_flat_always_valid() { for pos in [-1.5, -1.0, 0.0, 1.0, 1.5] { let mask = get_valid_action_mask(pos, 0.1); - assert!(mask[2], "Flat (exposure=0) should always be valid"); + assert!(mask[3], "Flat (exposure=0) should always be valid"); } } } diff --git a/crates/ml-core/src/common/action.rs b/crates/ml-core/src/common/action.rs index 4774588cc..aea190665 100644 --- a/crates/ml-core/src/common/action.rs +++ b/crates/ml-core/src/common/action.rs @@ -63,83 +63,122 @@ impl fmt::Display for OrderType { } } -/// Exposure level for position sizing (-100% to +100%) +/// Exposure level for position sizing (-100% to +100%). +/// +/// 7-level enum representing the composite direction × magnitude outcome. +/// The 4-branch DQN produces `direction(3) × magnitude(3)` but three +/// Flat × {Small,Half,Full} combos all map to 0.0 target exposure, so +/// they collapse into a single `Flat` variant. +/// +/// Total factored actions: 7 × 3 (order) × 3 (urgency) = 63. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ExposureLevel { - /// 4-branch factored DQN: discriminant = dir * 3 + mag - /// dir: 0=Short(-1), 1=Flat(0), 2=Long(+1) - /// mag: 0=Small(0.25), 1=Half(0.50), 2=Full(1.00) - /// - /// LEGACY NAMES: variant names reflect the old flat 9-level model. - /// Actual position = direction × magnitude (see target_exposure()). - Short100 = 0, // dir=0(Short), mag=0(Small) → -0.25 - Short75 = 1, // dir=0(Short), mag=1(Half) → -0.50 - Short50 = 2, // dir=0(Short), mag=2(Full) → -1.00 - Short25 = 3, // dir=1(Flat), mag=0(Small) → 0 - Flat = 4, // dir=1(Flat), mag=1(Half) → 0 - Long25 = 5, // dir=1(Flat), mag=2(Full) → 0 - Long50 = 6, // dir=2(Long), mag=0(Small) → +0.25 - Long75 = 7, // dir=2(Long), mag=1(Half) → +0.50 - Long100 = 8, // dir=2(Long), mag=2(Full) → +1.00 + ShortSmall = 0, // -0.25 + ShortHalf = 1, // -0.50 + ShortFull = 2, // -1.00 + Flat = 3, // 0.00 + LongSmall = 4, // +0.25 + LongHalf = 5, // +0.50 + LongFull = 6, // +1.00 } impl ExposureLevel { - /// Target exposure for the 4-branch factored DQN. - /// - /// Decodes from composite index `dir * 3 + mag`: - /// direction: [-1.0, 0.0, +1.0] - /// magnitude: [0.25, 0.50, 1.00] - /// result: direction × magnitude + /// Target exposure as a fraction of max position. pub fn target_exposure(&self) -> f64 { - let idx = *self as usize; - let dir = idx / 3; - let mag = idx % 3; - let direction: f64 = match dir { - 0 => -1.0, - 2 => 1.0, - _ => 0.0, - }; - let magnitude: f64 = match mag { - 0 => 0.25, - 1 => 0.50, - _ => 1.00, - }; - direction * magnitude + match self { + ExposureLevel::ShortSmall => -0.25, + ExposureLevel::ShortHalf => -0.50, + ExposureLevel::ShortFull => -1.00, + ExposureLevel::Flat => 0.00, + ExposureLevel::LongSmall => 0.25, + ExposureLevel::LongHalf => 0.50, + ExposureLevel::LongFull => 1.00, + } } - /// Convert from composite index (0-8): dir * 3 + mag + /// Convert from index (0-6). pub fn from_index(idx: usize) -> Result { match idx { - 0 => Ok(ExposureLevel::Short100), - 1 => Ok(ExposureLevel::Short75), - 2 => Ok(ExposureLevel::Short50), - 3 => Ok(ExposureLevel::Short25), - 4 => Ok(ExposureLevel::Flat), - 5 => Ok(ExposureLevel::Long25), - 6 => Ok(ExposureLevel::Long50), - 7 => Ok(ExposureLevel::Long75), - 8 => Ok(ExposureLevel::Long100), + 0 => Ok(ExposureLevel::ShortSmall), + 1 => Ok(ExposureLevel::ShortHalf), + 2 => Ok(ExposureLevel::ShortFull), + 3 => Ok(ExposureLevel::Flat), + 4 => Ok(ExposureLevel::LongSmall), + 5 => Ok(ExposureLevel::LongHalf), + 6 => Ok(ExposureLevel::LongFull), _ => Err(MLError::InvalidInput(format!( - "Invalid exposure level index: {} (expected 0-8)", + "Invalid exposure level index: {} (expected 0-6)", idx ))), } } + + /// Direction component: 0=Short, 1=Flat, 2=Long. + pub fn direction(&self) -> u8 { + match self { + ExposureLevel::ShortSmall | ExposureLevel::ShortHalf | ExposureLevel::ShortFull => 0, + ExposureLevel::Flat => 1, + ExposureLevel::LongSmall | ExposureLevel::LongHalf | ExposureLevel::LongFull => 2, + } + } + + /// Magnitude component: 0=Small, 1=Half, 2=Full, 3=N/A (Flat). + pub fn magnitude(&self) -> u8 { + match self { + ExposureLevel::ShortSmall | ExposureLevel::LongSmall => 0, + ExposureLevel::ShortHalf | ExposureLevel::LongHalf => 1, + ExposureLevel::ShortFull | ExposureLevel::LongFull => 2, + ExposureLevel::Flat => 3, + } + } + + /// Construct from direction (0-2) and magnitude (0-2) branch outputs. + /// When dir=1 (Flat), magnitude is ignored and Flat is returned. + pub fn from_dir_mag(dir: usize, mag: usize) -> Result { + match dir { + 0 => match mag { + 0 => Ok(ExposureLevel::ShortSmall), + 1 => Ok(ExposureLevel::ShortHalf), + 2 => Ok(ExposureLevel::ShortFull), + _ => Err(MLError::InvalidInput(format!("Invalid magnitude: {}", mag))), + }, + 1 => Ok(ExposureLevel::Flat), + 2 => match mag { + 0 => Ok(ExposureLevel::LongSmall), + 1 => Ok(ExposureLevel::LongHalf), + 2 => Ok(ExposureLevel::LongFull), + _ => Err(MLError::InvalidInput(format!("Invalid magnitude: {}", mag))), + }, + _ => Err(MLError::InvalidInput(format!("Invalid direction: {}", dir))), + } + } + + /// Check if this is a buy (long direction). + pub fn is_buy(&self) -> bool { + matches!(self, ExposureLevel::LongSmall | ExposureLevel::LongHalf | ExposureLevel::LongFull) + } + + /// Check if this is a sell (short direction). + pub fn is_sell(&self) -> bool { + matches!(self, ExposureLevel::ShortSmall | ExposureLevel::ShortHalf | ExposureLevel::ShortFull) + } + + /// Check if this is hold (flat). + pub fn is_hold(&self) -> bool { + matches!(self, ExposureLevel::Flat) + } } impl fmt::Display for ExposureLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Display as 4-branch dir×mag combo for clarity match self { - ExposureLevel::Short100 => write!(f, "S25"), // Short×Small - ExposureLevel::Short75 => write!(f, "S50"), // Short×Half - ExposureLevel::Short50 => write!(f, "S100"), // Short×Full - ExposureLevel::Short25 => write!(f, "F25"), // Flat×Small - ExposureLevel::Flat => write!(f, "F50"), // Flat×Half - ExposureLevel::Long25 => write!(f, "F100"), // Flat×Full - ExposureLevel::Long50 => write!(f, "L25"), // Long×Small - ExposureLevel::Long75 => write!(f, "L50"), // Long×Half - ExposureLevel::Long100 => write!(f, "L100"), // Long×Full + ExposureLevel::ShortSmall => write!(f, "ShortSmall"), + ExposureLevel::ShortHalf => write!(f, "ShortHalf"), + ExposureLevel::ShortFull => write!(f, "ShortFull"), + ExposureLevel::Flat => write!(f, "Flat"), + ExposureLevel::LongSmall => write!(f, "LongSmall"), + ExposureLevel::LongHalf => write!(f, "LongHalf"), + ExposureLevel::LongFull => write!(f, "LongFull"), } } } @@ -188,14 +227,13 @@ impl fmt::Display for Urgency { /// Factored trading action combining exposure, order type, and urgency. /// -/// 9 exposure levels x 3 order types x 3 urgency levels = 81 actions. -/// Index mapping: `index = exposure * 9 + order * 3 + urgency` (0-80). +/// 7 exposure levels x 3 order types x 3 urgency levels = 63 actions. +/// Index mapping: `index = exposure * 9 + order * 3 + urgency` (0-62). /// -/// NOTE: The CUDA training pipeline uses a 4-branch architecture -/// (direction × magnitude × order × urgency) where the first two branches -/// correspond to the `ExposureLevel` enum (9 = 3×3 combinations). -// TODO: update for 4-branch — split `exposure` into `direction` + `magnitude` -// fields once all ~64 callers are migrated in a dedicated refactor. +/// The CUDA training pipeline uses a 4-branch architecture +/// (direction × magnitude × order × urgency). Direction and magnitude +/// are collapsed into the 7-level `ExposureLevel` (Flat absorbs all +/// three Flat×{Small,Half,Full} combos into one variant). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct FactoredAction { pub exposure: ExposureLevel, @@ -213,12 +251,12 @@ impl FactoredAction { } } - /// Map action index (0-80) to (exposure, order, urgency) + /// Map action index (0-62) to (exposure, order, urgency). /// Index = exposure * 9 + order * 3 + urgency pub fn from_index(idx: usize) -> Result { - if idx >= 81 { + if idx >= 63 { return Err(MLError::InvalidInput(format!( - "Action index {} out of bounds (0-80)", + "Action index {} out of bounds (0-62)", idx ))); } @@ -234,7 +272,7 @@ impl FactoredAction { }) } - /// Map (exposure, order, urgency) to action index (0-80) + /// Map (exposure, order, urgency) to action index (0-62). /// Index = exposure * 9 + order * 3 + urgency pub fn to_index(&self) -> usize { let exposure_idx = self.exposure as usize; @@ -260,37 +298,31 @@ impl FactoredAction { } /// Create a FactoredAction from a TradingAction. - /// Buy → Long+Full(+1.0), Sell → Short+Full(-1.0), Hold → Flat+Half(0). - /// - /// In the 4-branch encoding: Long+Full = Long100(=8), Short+Full = Short50(=2), - /// Flat+Half = Flat(=4). Legacy variant names don't match 4-branch semantics. + /// Buy → LongFull(+1.0), Sell → ShortFull(-1.0), Hold → Flat(0). pub fn from_trading_action(action: crate::trading_action::TradingAction) -> Self { use crate::trading_action::TradingAction; match action { TradingAction::Buy => { - Self::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal) // Long+Full = +1.0 + Self::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal) } TradingAction::Sell => { - Self::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal) // Short+Full = -1.0 + Self::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal) } TradingAction::Hold => { - Self::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal) // Flat+Half = 0 + Self::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal) } } } /// Convert FactoredAction to TradingAction for reward calculation. - /// - /// Maps exposure levels to simple Buy/Sell/Hold actions: - /// - Long100, Long50 -> Buy - /// - Flat -> Hold - /// - Short50, Short100 -> Sell pub fn to_trading_action(&self) -> crate::trading_action::TradingAction { use crate::trading_action::TradingAction; - match self.exposure { - ExposureLevel::Long100 | ExposureLevel::Long75 | ExposureLevel::Long50 | ExposureLevel::Long25 => TradingAction::Buy, - ExposureLevel::Flat => TradingAction::Hold, - ExposureLevel::Short25 | ExposureLevel::Short50 | ExposureLevel::Short75 | ExposureLevel::Short100 => TradingAction::Sell, + if self.exposure.is_buy() { + TradingAction::Buy + } else if self.exposure.is_sell() { + TradingAction::Sell + } else { + TradingAction::Hold } } @@ -310,28 +342,19 @@ impl FactoredAction { trade_value * self.transaction_cost() } - /// Check if this action is a buy (long direction, dir=2, indices 6-8) + /// Check if this action is a buy (long direction). pub fn is_buy(&self) -> bool { - matches!( - self.exposure, - ExposureLevel::Long50 | ExposureLevel::Long75 | ExposureLevel::Long100 - ) + self.exposure.is_buy() } - /// Check if this action is a sell (short direction, dir=0, indices 0-2) + /// Check if this action is a sell (short direction). pub fn is_sell(&self) -> bool { - matches!( - self.exposure, - ExposureLevel::Short100 | ExposureLevel::Short75 | ExposureLevel::Short50 - ) + self.exposure.is_sell() } - /// Check if this action is neutral (flat direction, dir=1, indices 3-5) + /// Check if this action is neutral (flat). pub fn is_hold(&self) -> bool { - matches!( - self.exposure, - ExposureLevel::Short25 | ExposureLevel::Flat | ExposureLevel::Long25 - ) + self.exposure.is_hold() } /// Convert action to position delta @@ -363,8 +386,8 @@ mod tests { use super::*; #[test] - fn test_factored_action_round_trip_all_81() { - for idx in 0..81 { + fn test_factored_action_round_trip_all_63() { + for idx in 0..63 { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.to_index(), @@ -377,17 +400,17 @@ mod tests { #[test] fn test_factored_action_out_of_bounds() { - assert!(FactoredAction::from_index(81).is_err()); + assert!(FactoredAction::from_index(63).is_err()); assert!(FactoredAction::from_index(100).is_err()); } #[test] fn test_factored_action_to_trading_action() { use crate::trading_action::TradingAction; - let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); assert_eq!(buy.to_trading_action(), TradingAction::Buy); let sell = - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal); assert_eq!(sell.to_trading_action(), TradingAction::Sell); let hold = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); assert_eq!(hold.to_trading_action(), TradingAction::Hold); @@ -412,15 +435,13 @@ mod tests { #[test] fn test_exposure_level_values() { - assert_eq!(ExposureLevel::Short100 as usize, 0); - assert_eq!(ExposureLevel::Short75 as usize, 1); - assert_eq!(ExposureLevel::Short50 as usize, 2); - assert_eq!(ExposureLevel::Short25 as usize, 3); - assert_eq!(ExposureLevel::Flat as usize, 4); - assert_eq!(ExposureLevel::Long25 as usize, 5); - assert_eq!(ExposureLevel::Long50 as usize, 6); - assert_eq!(ExposureLevel::Long75 as usize, 7); - assert_eq!(ExposureLevel::Long100 as usize, 8); + assert_eq!(ExposureLevel::ShortSmall as usize, 0); + assert_eq!(ExposureLevel::ShortHalf as usize, 1); + assert_eq!(ExposureLevel::ShortFull as usize, 2); + assert_eq!(ExposureLevel::Flat as usize, 3); + assert_eq!(ExposureLevel::LongSmall as usize, 4); + assert_eq!(ExposureLevel::LongHalf as usize, 5); + assert_eq!(ExposureLevel::LongFull as usize, 6); } #[test] @@ -432,12 +453,13 @@ mod tests { #[test] fn test_target_exposure() { - // 4-branch: dir × mag formula - assert_eq!(ExposureLevel::Short100.target_exposure(), -0.25); // Short×Small - assert_eq!(ExposureLevel::Short50.target_exposure(), -1.0); // Short×Full - assert_eq!(ExposureLevel::Flat.target_exposure(), 0.0); // Flat×Half - assert_eq!(ExposureLevel::Long50.target_exposure(), 0.25); // Long×Small - assert_eq!(ExposureLevel::Long100.target_exposure(), 1.0); // Long×Full + assert_eq!(ExposureLevel::ShortSmall.target_exposure(), -0.25); + assert_eq!(ExposureLevel::ShortHalf.target_exposure(), -0.50); + assert_eq!(ExposureLevel::ShortFull.target_exposure(), -1.0); + assert_eq!(ExposureLevel::Flat.target_exposure(), 0.0); + assert_eq!(ExposureLevel::LongSmall.target_exposure(), 0.25); + assert_eq!(ExposureLevel::LongHalf.target_exposure(), 0.50); + assert_eq!(ExposureLevel::LongFull.target_exposure(), 1.0); } #[test] @@ -449,13 +471,13 @@ mod tests { #[test] fn test_is_buy_sell_hold() { - let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); assert!(buy.is_buy()); assert!(!buy.is_sell()); assert!(!buy.is_hold()); let sell = - FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal); assert!(sell.is_sell()); assert!(!sell.is_buy()); assert!(!sell.is_hold()); @@ -469,7 +491,7 @@ mod tests { #[test] fn test_position_delta() { let action = - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let delta = action.to_position_delta(0.0, 2.0); assert!((delta - 2.0).abs() < 1e-10); } @@ -477,7 +499,7 @@ mod tests { #[test] fn test_calculate_transaction_cost() { let action = - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Aggressive); + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive); let cost = action.calculate_transaction_cost(10_000.0); assert!((cost - 15.0).abs() < 1e-10); } @@ -485,8 +507,33 @@ mod tests { #[test] fn test_display() { let action = - FactoredAction::new(ExposureLevel::Long50, OrderType::LimitMaker, Urgency::Patient); + FactoredAction::new(ExposureLevel::LongSmall, OrderType::LimitMaker, Urgency::Patient); let s = format!("{}", action); - assert_eq!(s, "L25+LimitMaker+Patient"); // Long50 displays as L25 (Long×Small) + assert_eq!(s, "LongSmall+LimitMaker+Patient"); + } + + #[test] + fn test_from_dir_mag() { + assert_eq!(ExposureLevel::from_dir_mag(0, 0).unwrap(), ExposureLevel::ShortSmall); + assert_eq!(ExposureLevel::from_dir_mag(0, 1).unwrap(), ExposureLevel::ShortHalf); + assert_eq!(ExposureLevel::from_dir_mag(0, 2).unwrap(), ExposureLevel::ShortFull); + assert_eq!(ExposureLevel::from_dir_mag(1, 0).unwrap(), ExposureLevel::Flat); + assert_eq!(ExposureLevel::from_dir_mag(1, 1).unwrap(), ExposureLevel::Flat); + assert_eq!(ExposureLevel::from_dir_mag(1, 2).unwrap(), ExposureLevel::Flat); + assert_eq!(ExposureLevel::from_dir_mag(2, 0).unwrap(), ExposureLevel::LongSmall); + assert_eq!(ExposureLevel::from_dir_mag(2, 1).unwrap(), ExposureLevel::LongHalf); + assert_eq!(ExposureLevel::from_dir_mag(2, 2).unwrap(), ExposureLevel::LongFull); + } + + #[test] + fn test_direction_magnitude() { + assert_eq!(ExposureLevel::ShortSmall.direction(), 0); + assert_eq!(ExposureLevel::ShortSmall.magnitude(), 0); + assert_eq!(ExposureLevel::ShortFull.direction(), 0); + assert_eq!(ExposureLevel::ShortFull.magnitude(), 2); + assert_eq!(ExposureLevel::Flat.direction(), 1); + assert_eq!(ExposureLevel::Flat.magnitude(), 3); // N/A + assert_eq!(ExposureLevel::LongFull.direction(), 2); + assert_eq!(ExposureLevel::LongFull.magnitude(), 2); } } diff --git a/crates/ml-core/src/order_router.rs b/crates/ml-core/src/order_router.rs index 59eac46a9..dcb631087 100644 --- a/crates/ml-core/src/order_router.rs +++ b/crates/ml-core/src/order_router.rs @@ -63,7 +63,7 @@ mod tests { #[test] fn test_tight_spread_uses_limit_maker() { let action = OrderRouter::route( - ExposureLevel::Long100, + ExposureLevel::LongFull, 0.5, // spread 1.0, // median_spread (spread < median) 0.01, // vol @@ -75,7 +75,7 @@ mod tests { #[test] fn test_wide_spread_uses_market() { let action = OrderRouter::route( - ExposureLevel::Short100, + ExposureLevel::ShortFull, 2.5, // spread 1.0, // median_spread (spread > 2x median) 0.01, // vol @@ -99,7 +99,7 @@ mod tests { #[test] fn test_high_vol_aggressive() { let action = OrderRouter::route( - ExposureLevel::Long50, + ExposureLevel::LongSmall, 1.0, // spread 1.0, // median_spread 0.05, // vol @@ -111,7 +111,7 @@ mod tests { #[test] fn test_low_vol_patient() { let action = OrderRouter::route( - ExposureLevel::Short50, + ExposureLevel::ShortFull, 1.0, // spread 1.0, // median_spread 0.005, // vol @@ -134,8 +134,8 @@ mod tests { #[test] fn test_route_default() { - let action = OrderRouter::route_default(ExposureLevel::Long100); - assert_eq!(action.exposure, ExposureLevel::Long100); + let action = OrderRouter::route_default(ExposureLevel::LongFull); + assert_eq!(action.exposure, ExposureLevel::LongFull); assert_eq!(action.order, OrderType::Market); assert_eq!(action.urgency, Urgency::Normal); } @@ -143,7 +143,7 @@ mod tests { #[test] fn test_zero_median_spread_defaults_to_market() { let action = OrderRouter::route( - ExposureLevel::Long100, + ExposureLevel::LongFull, 0.5, 0.0, // zero median 0.01, @@ -155,7 +155,7 @@ mod tests { #[test] fn test_zero_median_vol_defaults_to_normal() { let action = OrderRouter::route( - ExposureLevel::Long100, + ExposureLevel::LongFull, 1.0, 1.0, 0.01, @@ -166,7 +166,7 @@ mod tests { #[test] fn test_all_exposure_levels() { - for idx in 0..5 { + for idx in 0..7 { let exposure = ExposureLevel::from_index(idx).unwrap(); let action = OrderRouter::route_default(exposure); assert_eq!(action.exposure, exposure); diff --git a/crates/ml-core/src/portfolio_tracker.rs b/crates/ml-core/src/portfolio_tracker.rs index 60ffa8489..c12bc206a 100644 --- a/crates/ml-core/src/portfolio_tracker.rs +++ b/crates/ml-core/src/portfolio_tracker.rs @@ -202,7 +202,7 @@ impl PortfolioTracker { /// use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency}; /// /// let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); - /// let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + /// let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); /// tracker.execute_action(action, 100.0, 100.0); /// assert_eq!(tracker.position_size, 100.0); // Full long position /// ``` @@ -878,7 +878,7 @@ mod tests { tracker.position_size = 0.0; // peak_value should be 100K from initialization - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 100.0, 4.0); // Should refuse to open position when drawdown > 20% @@ -896,7 +896,7 @@ mod tests { let mut tracker = PortfolioTracker::new(100_000.0, 0.0, 0.0); // Open a long position first (at no drawdown) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 100.0, 4.0); assert!((tracker.current_position() - 4.0).abs() < f32::EPSILON); @@ -907,7 +907,7 @@ mod tests { // Total value = 50_000 + 4*100 = 50_400. Drawdown = 1 - 50400/100000 = 49.6% // Any action should force-close the position - let action2 = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action2 = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action2, 100.0, 4.0); // Should have force-closed to flat @@ -928,7 +928,7 @@ mod tests { tracker.cash = 85_000.0; tracker.position_size = 0.0; - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 100.0, 4.0); // Should allow the trade (drawdown < 20%) diff --git a/crates/ml-dqn/src/branching.rs b/crates/ml-dqn/src/branching.rs index cb3d802c6..6395d4c17 100644 --- a/crates/ml-dqn/src/branching.rs +++ b/crates/ml-dqn/src/branching.rs @@ -1236,8 +1236,8 @@ mod tests { let actions = BranchingDuelingQNetwork::greedy_branch_actions(&output, &stream)?; assert_eq!(actions.len(), 3); assert!( - *actions.get(0).unwrap_or(&99) < 5, - "Exposure action should be 0-4" + *actions.get(0).unwrap_or(&99) < 7, + "Exposure action should be 0-6" ); assert!( *actions.get(1).unwrap_or(&99) < 3, @@ -1252,7 +1252,7 @@ mod tests { #[test] fn test_decompose_compose_roundtrip() { - for idx in 0..81_u32 { + for idx in 0..63_u32 { let (e, o, u) = BranchingDuelingQNetwork::decompose_factored_action(idx as usize, 3, 3); let composed = BranchingDuelingQNetwork::compose_factored_action(e as u32, o as u32, u as u32, 3, 3); @@ -1263,8 +1263,8 @@ mod tests { #[test] fn test_decompose_actions_batch() -> anyhow::Result<()> { let stream = test_stream(); - // Action 0 = (0,0,0), Action 13 = (1,1,1), Action 44 = (4,2,2) - let actions = vec![0_u32, 13, 44]; + // Action 0 = (0,0,0), Action 13 = (1,1,1), Action 62 = (6,2,2) + let actions = vec![0_u32, 13, 62]; let branches = BranchingDuelingQNetwork::decompose_actions_batch(&actions, &stream, 3, 3)?; @@ -1281,7 +1281,7 @@ mod tests { .ok_or_else(|| anyhow::anyhow!("missing branch 2"))? .to_host(&stream)?; - assert_eq!(e, vec![0.0_f32, 1.0, 4.0]); + assert_eq!(e, vec![0.0_f32, 1.0, 6.0]); assert_eq!(o, vec![0.0_f32, 1.0, 2.0]); assert_eq!(u, vec![0.0_f32, 1.0, 2.0]); Ok(()) @@ -1290,13 +1290,13 @@ mod tests { #[test] fn test_decompose_actions_batch_gpu_matches_cpu() -> anyhow::Result<()> { let stream = test_stream(); - // Test all 45 factored actions -- GPU-native version must match CPU version - let all_actions: Vec = (0..45).collect(); + // Test all 63 factored actions -- GPU-native version must match CPU version + let all_actions: Vec = (0..63).collect(); let cpu_branches = BranchingDuelingQNetwork::decompose_actions_batch(&all_actions, &stream, 3, 3)?; let all_f32: Vec = all_actions.iter().map(|&x| x as f32).collect(); - let actions_tensor = GpuTensor::from_host(&all_f32, vec![45], &stream)?; + let actions_tensor = GpuTensor::from_host(&all_f32, vec![63], &stream)?; let gpu_branches = BranchingDuelingQNetwork::decompose_actions_batch_gpu(&actions_tensor, 3, 3, &stream)?; diff --git a/crates/ml-dqn/src/branching_composition_tests.rs b/crates/ml-dqn/src/branching_composition_tests.rs index 35f9974c0..83b429269 100644 --- a/crates/ml-dqn/src/branching_composition_tests.rs +++ b/crates/ml-dqn/src/branching_composition_tests.rs @@ -2,31 +2,31 @@ mod tests { #[test] fn test_factored_action_composition() { - // Verify: exposure * 9 + order * 3 + urgency covers 0-44 + // Verify: exposure * 9 + order * 3 + urgency covers 0-62 let mut seen = std::collections::HashSet::new(); - for exposure in 0..5_u32 { + for exposure in 0..7_u32 { for order in 0..3_u32 { for urgency in 0..3_u32 { let factored = exposure * 9 + order * 3 + urgency; - assert!(factored < 45, "factored {} out of range", factored); + assert!(factored < 63, "factored {} out of range", factored); seen.insert(factored); } } } - assert_eq!(seen.len(), 45, "Must cover all 45 factored actions"); + assert_eq!(seen.len(), 63, "Must cover all 63 factored actions"); } #[test] fn test_factored_action_decomposition() { // Verify round-trip: compose then decompose - for original in 0..45_u32 { + for original in 0..63_u32 { let exposure = original / 9; let order = (original % 9) / 3; let urgency = original % 3; let recomposed = exposure * 9 + order * 3 + urgency; assert_eq!(original, recomposed, "Round-trip failed for {}", original); - assert!(exposure < 5); + assert!(exposure < 7); assert!(order < 3); assert!(urgency < 3); } diff --git a/crates/ml-dqn/src/curiosity.rs b/crates/ml-dqn/src/curiosity.rs index 558a4aca6..18b798028 100644 --- a/crates/ml-dqn/src/curiosity.rs +++ b/crates/ml-dqn/src/curiosity.rs @@ -115,9 +115,9 @@ impl ForwardDynamicsModel { // One-hot encode action let action_idx = match action.exposure { - ExposureLevel::Short100 | ExposureLevel::Short75 | ExposureLevel::Short50 | ExposureLevel::Short25 => 0_usize, + ExposureLevel::ShortSmall | ExposureLevel::ShortHalf | ExposureLevel::ShortFull => 0_usize, ExposureLevel::Flat => 1_usize, - ExposureLevel::Long25 | ExposureLevel::Long50 | ExposureLevel::Long75 | ExposureLevel::Long100 => 2_usize, + ExposureLevel::LongSmall | ExposureLevel::LongHalf | ExposureLevel::LongFull => 2_usize, }; // Build input: [market_features | action_onehot] per sample @@ -304,7 +304,7 @@ mod tests { // Helper to create a test BUY action fn test_buy_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Aggressive) + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive) } fn make_stream() -> Arc { @@ -414,8 +414,8 @@ mod tests { let state = vec![0.0_f32; MARKET_DIM]; // Predict with different actions - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Aggressive); - let sell_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Aggressive); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive); + let sell_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Aggressive); let hold_action = FactoredAction::new(ExposureLevel::Flat, OrderType::LimitMaker, Urgency::Patient); let pred_buy = model.predict(&state, 1, MARKET_DIM, buy_action)?; diff --git a/crates/ml-dqn/src/dqn.rs b/crates/ml-dqn/src/dqn.rs index 7e0fb71a1..e2df92a68 100644 --- a/crates/ml-dqn/src/dqn.rs +++ b/crates/ml-dqn/src/dqn.rs @@ -245,7 +245,7 @@ impl Default for DQNConfig { fn default() -> Self { Self { state_dim: 48, // 40 market + 3 portfolio = 43, padded to 48 for tensor core alignment - num_actions: 9, // 9 exposure levels (25% steps) + num_actions: 7, // 7 exposure levels (ShortSmall..LongFull + Flat) hidden_dims: vec![256, 256], learning_rate: 3e-5, // Conservative default for stable training (matches conservative()) gamma: 0.95, @@ -1514,10 +1514,10 @@ impl DQN { if exp_random && ord_random && urg_random { // All branches random — skip forward pass. - // 4-branch: exposure = dir*3 + mag (9 combos), order(3), urgency(3). + // 4-branch: dir(3) × mag(3) → 7-level ExposureLevel, order(3), urgency(3). let dir = rng.gen_range(0..3_usize); let mag = rng.gen_range(0..3_usize); - let exposure = ExposureLevel::from_index(dir * 3 + mag)?; + let exposure = ExposureLevel::from_dir_mag(dir, mag)?; let order = OrderType::from_index(rng.gen_range(0..3_usize))?; let urgency = Urgency::from_index(rng.gen_range(0..3_usize))?; FactoredAction { exposure, order, urgency } @@ -1532,15 +1532,15 @@ impl DQN { let output = branching_net.forward_branches_eval(&state_tensor)?; let greedy = super::branching::BranchingDuelingQNetwork::greedy_branch_actions(&output, &self.stream)?; - // 4-branch: greedy[0]=dir(0-2), greedy[1]=mag(0-2), composite = dir*3+mag + // 4-branch: greedy[0]=dir(0-2), greedy[1]=mag(0-2) → 7-level ExposureLevel let exposure = if exp_random { let dir = rng.gen_range(0..3_usize); let mag = rng.gen_range(0..3_usize); - ExposureLevel::from_index(dir * 3 + mag)? + ExposureLevel::from_dir_mag(dir, mag)? } else { let dir = greedy.first().copied().unwrap_or(1) as usize; let mag = greedy.get(1).copied().unwrap_or(1) as usize; - ExposureLevel::from_index(dir * 3 + mag)? + ExposureLevel::from_dir_mag(dir, mag)? }; // 4-branch greedy: [dir, mag, order, urgency] → order at [2], urgency at [3] let order = if ord_random { @@ -1720,7 +1720,7 @@ impl DQN { // 4-branch: exposure = dir*3 + mag (9 combos), order(3), urgency(3) let dir = rng.gen_range(0..3_usize); let mag = rng.gen_range(0..3_usize); - let exposure = ExposureLevel::from_index(dir * 3 + mag)?; + let exposure = ExposureLevel::from_dir_mag(dir, mag)?; let order = OrderType::from_index(rng.gen_range(0..3_usize))?; let urgency = Urgency::from_index(rng.gen_range(0..3_usize))?; let uniform_conf = (1.0_f32 / 81.0).clamp(0.5, 0.95); @@ -1756,11 +1756,11 @@ impl DQN { let exposure = if exp_random { let dir = rng.gen_range(0..3_usize); let mag = rng.gen_range(0..3_usize); - ExposureLevel::from_index(dir * 3 + mag)? + ExposureLevel::from_dir_mag(dir, mag)? } else { let dir = greedy.first().copied().unwrap_or(1) as usize; let mag = greedy.get(1).copied().unwrap_or(1) as usize; - ExposureLevel::from_index(dir * 3 + mag)? + ExposureLevel::from_dir_mag(dir, mag)? }; // 4-branch greedy: [dir, mag, order, urgency] -> order at [2], urgency at [3] let order = if ord_random { @@ -1925,7 +1925,7 @@ impl DQN { // 4-branch: [dir, mag, order, urgency] → exposure = dir*3 + mag let dir = branch_actions.first().copied().unwrap_or(1) as usize; let mag = branch_actions.get(1).copied().unwrap_or(1) as usize; - let exposure = ExposureLevel::from_index(dir * 3 + mag)?; + let exposure = ExposureLevel::from_dir_mag(dir, mag)?; let order = OrderType::from_index(branch_actions.get(2).copied().unwrap_or(0) as usize)?; let urgency = Urgency::from_index(branch_actions.get(3).copied().unwrap_or(1) as usize)?; diff --git a/crates/ml-dqn/src/evaluation/engine.rs b/crates/ml-dqn/src/evaluation/engine.rs index 27c625e2b..e3623708b 100644 --- a/crates/ml-dqn/src/evaluation/engine.rs +++ b/crates/ml-dqn/src/evaluation/engine.rs @@ -425,8 +425,8 @@ mod tests { fn factored_same_exposure_no_trade() { let mut engine = EvaluationEngine::new(10000.0); let b = bar(100.0); - engine.process_bar_factored(0, &b, &market_action(ExposureLevel::Long100)); - engine.process_bar_factored(1, &b, &market_action(ExposureLevel::Long100)); + engine.process_bar_factored(0, &b, &market_action(ExposureLevel::LongFull)); + engine.process_bar_factored(1, &b, &market_action(ExposureLevel::LongFull)); assert_eq!( engine.trades.len(), 0, @@ -437,8 +437,8 @@ mod tests { #[test] fn factored_partial_close_generates_trade() { let mut engine = EvaluationEngine::new(10000.0); - engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::Long100)); - engine.process_bar_factored(1, &bar(110.0), &market_action(ExposureLevel::Long50)); + engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::LongFull)); + engine.process_bar_factored(1, &bar(110.0), &market_action(ExposureLevel::LongSmall)); assert_eq!( engine.trades.len(), 1, @@ -454,8 +454,8 @@ mod tests { #[test] fn factored_reversal_generates_trade() { let mut engine = EvaluationEngine::new(10000.0); - engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::Long100)); - engine.process_bar_factored(1, &bar(105.0), &market_action(ExposureLevel::Short50)); + engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::LongFull)); + engine.process_bar_factored(1, &bar(105.0), &market_action(ExposureLevel::ShortFull)); assert!( !engine.trades.is_empty(), "Reversal should generate at least 1 trade" @@ -467,7 +467,7 @@ mod tests { #[test] fn factored_flat_from_long_closes() { let mut engine = EvaluationEngine::new(10000.0); - engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::Long100)); + engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::LongFull)); engine.process_bar_factored(1, &bar(95.0), &market_action(ExposureLevel::Flat)); assert_eq!(engine.trades.len(), 1); assert!( @@ -480,7 +480,7 @@ mod tests { #[test] fn factored_close_at_end() { let mut engine = EvaluationEngine::new(10000.0); - engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::Short50)); + engine.process_bar_factored(0, &bar(100.0), &market_action(ExposureLevel::ShortFull)); engine.close_factored_position(1, &bar(90.0)); assert_eq!(engine.trades.len(), 1); assert!( @@ -497,7 +497,7 @@ mod tests { engine.process_bar_factored( i, &bar(100.0 + i as f32), - &market_action(ExposureLevel::Long100), + &market_action(ExposureLevel::LongFull), ); } assert_eq!(engine.trades.len(), 0); @@ -510,9 +510,9 @@ mod tests { let mut engine = EvaluationEngine::new(10000.0); for i in 0..10 { let action = if i % 2 == 0 { - market_action(ExposureLevel::Long100) + market_action(ExposureLevel::LongFull) } else { - market_action(ExposureLevel::Short100) + market_action(ExposureLevel::ShortSmall) }; engine.process_bar_factored(i, &bar(100.0), &action); } diff --git a/crates/ml-dqn/src/multi_asset.rs b/crates/ml-dqn/src/multi_asset.rs index 676534a31..794abfb8d 100644 --- a/crates/ml-dqn/src/multi_asset.rs +++ b/crates/ml-dqn/src/multi_asset.rs @@ -533,7 +533,7 @@ mod tests { let mut tracker = MultiAssetPortfolioTracker::new(symbols.clone(), Decimal::from(10_000)); // ES: Long 10 at 4500 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(&Symbol::new("ES_FUT"), action, 4500.0, 10.0); let prices = HashMap::from([ diff --git a/crates/ml-dqn/src/reward.rs b/crates/ml-dqn/src/reward.rs index 72f3c7e5b..1cf3e8086 100644 --- a/crates/ml-dqn/src/reward.rs +++ b/crates/ml-dqn/src/reward.rs @@ -1021,7 +1021,7 @@ mod tests { // Test Market order (0.15% fee) let market_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); @@ -1037,7 +1037,7 @@ mod tests { // Test LimitMaker order (0.05% fee) with Patient urgency (0.5x) let limit_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::LimitMaker, Urgency::Patient, ); @@ -1053,7 +1053,7 @@ mod tests { // Test IoC order (0.10% fee) with Aggressive urgency (1.5x) let ioc_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::IoC, Urgency::Aggressive, ); @@ -1108,7 +1108,7 @@ mod tests { // Market order action let market_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); diff --git a/crates/ml-ppo/src/action_masking.rs b/crates/ml-ppo/src/action_masking.rs index 949bb2d04..7fe824d0f 100644 --- a/crates/ml-ppo/src/action_masking.rs +++ b/crates/ml-ppo/src/action_masking.rs @@ -41,25 +41,27 @@ pub fn create_action_mask(current_position: f64, max_position: f64, num_actions: if current_position - 1.0 < -max_position { mask[1] = false; } - } else if num_actions == 45 { + } else if num_actions == 63 { for action_idx in 0..num_actions { let exposure_idx = action_idx / 9; match exposure_idx { - 3 | 4 => { + // Long actions (LongSmall=4, LongHalf=5, LongFull=6) + 4 | 5 | 6 => { if current_position >= max_position { if let Some(m) = mask.get_mut(action_idx) { *m = false; } } } - 0 | 1 => { + // Short actions (ShortSmall=0, ShortHalf=1, ShortFull=2) + 0 | 1 | 2 => { if current_position <= -max_position { if let Some(m) = mask.get_mut(action_idx) { *m = false; } } } - _ => {} + _ => {} // Flat=3 } } } else { @@ -143,56 +145,50 @@ mod tests { } #[test] - fn test_create_action_mask_45_flat_position() { - let mask = create_action_mask(0.0, 2.0, 45); - assert_eq!(mask.len(), 45); + fn test_create_action_mask_63_flat_position() { + let mask = create_action_mask(0.0, 2.0, 63); + assert_eq!(mask.len(), 63); assert!(mask.iter().all(|&v| v)); } #[test] - fn test_create_action_mask_45_at_max_position() { - let mask = create_action_mask(2.0, 2.0, 45); - assert_eq!(mask.len(), 45); - for i in 0..9 { - assert_eq!(mask.get(i).copied().unwrap_or(false), true); - } - for i in 9..18 { - assert_eq!(mask.get(i).copied().unwrap_or(false), true); - } - for i in 18..27 { + fn test_create_action_mask_63_at_max_position() { + let mask = create_action_mask(2.0, 2.0, 63); + assert_eq!(mask.len(), 63); + // Short actions (exposure 0,1,2) should be valid + for i in 0..27 { assert_eq!(mask.get(i).copied().unwrap_or(false), true); } + // Flat (exposure 3) should be valid for i in 27..36 { - assert_eq!(mask.get(i).copied().unwrap_or(true), false); + assert_eq!(mask.get(i).copied().unwrap_or(false), true); } - for i in 36..45 { + // Long actions (exposure 4,5,6) should be masked + for i in 36..63 { assert_eq!(mask.get(i).copied().unwrap_or(true), false); } } #[test] - fn test_create_action_mask_45_at_min_position() { - let mask = create_action_mask(-2.0, 2.0, 45); - for i in 0..9 { + fn test_create_action_mask_63_at_min_position() { + let mask = create_action_mask(-2.0, 2.0, 63); + // Short actions (exposure 0,1,2) should be masked + for i in 0..27 { assert_eq!(mask.get(i).copied().unwrap_or(true), false); } - for i in 9..18 { - assert_eq!(mask.get(i).copied().unwrap_or(true), false); - } - for i in 18..27 { - assert_eq!(mask.get(i).copied().unwrap_or(false), true); - } + // Flat (exposure 3) should be valid for i in 27..36 { assert_eq!(mask.get(i).copied().unwrap_or(false), true); } - for i in 36..45 { + // Long actions (exposure 4,5,6) should be valid + for i in 36..63 { assert_eq!(mask.get(i).copied().unwrap_or(false), true); } } #[test] - fn test_create_action_mask_45_partial_position() { - let mask = create_action_mask(1.0, 2.0, 45); + fn test_create_action_mask_63_partial_position() { + let mask = create_action_mask(1.0, 2.0, 63); assert!(mask.iter().all(|&v| v)); } @@ -209,18 +205,18 @@ mod tests { } #[test] - fn test_45_action_masking_canonical_layout() { - // 45-action layout: 5 exposure levels × 9 sub-actions each + fn test_63_action_masking_canonical_layout() { + // 63-action layout: 7 exposure levels × 9 sub-actions each // exposure_idx = action_idx / 9: - // 0 = Short100, 1 = Short50, 2 = Flat, 3 = Long50, 4 = Long100 + // 0=ShortSmall, 1=ShortHalf, 2=ShortFull, 3=Flat, 4=LongSmall, 5=LongHalf, 6=LongFull let max_position = 5.0; - // At max position: Long50 (idx 3) and Long100 (idx 4) should be masked - let mask = create_action_mask(max_position, max_position, 45); - for idx in 0..45 { + // At max position: Long actions (idx 4,5,6) should be masked + let mask = create_action_mask(max_position, max_position, 63); + for idx in 0..63 { let exposure_idx = idx / 9; match exposure_idx { - 3 | 4 => { + 4 | 5 | 6 => { assert!( !mask[idx], "Long action {} (exposure_idx={}) should be masked at max position", @@ -237,12 +233,12 @@ mod tests { } } - // At min position: Short100 (idx 0) and Short50 (idx 1) should be masked - let mask = create_action_mask(-max_position, max_position, 45); - for idx in 0..45 { + // At min position: Short actions (idx 0,1,2) should be masked + let mask = create_action_mask(-max_position, max_position, 63); + for idx in 0..63 { let exposure_idx = idx / 9; match exposure_idx { - 0 | 1 => { + 0 | 1 | 2 => { assert!( !mask[idx], "Short action {} (exposure_idx={}) should be masked at min position", diff --git a/crates/ml-ppo/src/action_space.rs b/crates/ml-ppo/src/action_space.rs index 95c882426..9cf649ca6 100644 --- a/crates/ml-ppo/src/action_space.rs +++ b/crates/ml-ppo/src/action_space.rs @@ -2,7 +2,7 @@ //! //! This module provides a unified interface for both discrete and continuous action spaces. //! It enables the same PPO framework to handle: -//! - Discrete actions: 45-action factored space (5x3x3 = exposure x order x urgency) +//! - Discrete actions: 63-action factored space (7x3x3 = exposure x order x urgency) //! - Continuous actions: Gaussian policy for position sizing (0.0 to 1.0) use ml_core::action_space::FactoredAction; @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; /// Action space type discriminator #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ActionType { - /// Discrete action space (45-action factored space) + /// Discrete action space (63-action factored space) Discrete, /// Continuous action space (Gaussian policy) Continuous, @@ -22,7 +22,7 @@ pub enum ActionType { /// Unified action space supporting both discrete and continuous actions #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ActionSpace { - /// Discrete trading action (45-action factored space) + /// Discrete trading action (63-action factored space) Discrete(FactoredAction), /// Continuous trading action (position sizing) Continuous(ContinuousAction), @@ -82,7 +82,7 @@ impl ActionSpace { /// Validate action is within bounds pub fn is_valid(&self) -> bool { match self { - ActionSpace::Discrete(action) => action.to_index() < 45, + ActionSpace::Discrete(action) => action.to_index() < 63, ActionSpace::Continuous(action) => action.is_valid(), } } @@ -125,7 +125,7 @@ mod tests { #[test] fn test_action_type() { let discrete = ActionSpace::discrete(FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, )); @@ -138,7 +138,7 @@ mod tests { #[test] fn test_as_discrete() { let action = FactoredAction::new( - ExposureLevel::Short100, + ExposureLevel::ShortSmall, OrderType::IoC, Urgency::Patient, ); @@ -171,12 +171,12 @@ mod tests { #[test] fn test_description() { let discrete = ActionSpace::discrete(FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, )); let desc = discrete.description(); - assert!(desc.contains("Long100")); + assert!(desc.contains("LongFull")); assert!(desc.contains("Market")); assert!(desc.contains("Aggressive")); @@ -187,7 +187,7 @@ mod tests { #[test] fn test_all_discrete_actions() -> Result<(), MLError> { - for idx in 0..45 { + for idx in 0..63 { let action = FactoredAction::from_index(idx)?; let action_space = ActionSpace::discrete(action); assert!(action_space.is_valid()); diff --git a/crates/ml-ppo/src/adaptive_entropy.rs b/crates/ml-ppo/src/adaptive_entropy.rs index 3b0a5eab5..feb757ee9 100644 --- a/crates/ml-ppo/src/adaptive_entropy.rs +++ b/crates/ml-ppo/src/adaptive_entropy.rs @@ -27,7 +27,7 @@ impl Default for AdaptiveEntropyConfig { initial_alpha: 0.05, target_ratio: 0.5, alpha_lr: 3e-4, - num_actions: 45, + num_actions: 63, } } } @@ -155,7 +155,7 @@ mod tests { initial_alpha: 0.05, target_ratio: 0.5, alpha_lr: 0.01, - num_actions: 45, + num_actions: 63, }; let mut ae = AdaptiveEntropyCoeff::new(&config)?; let initial_alpha = ae.alpha()?; @@ -180,7 +180,7 @@ mod tests { initial_alpha: 0.05, target_ratio: 0.5, alpha_lr: 0.01, - num_actions: 45, + num_actions: 63, }; let mut ae = AdaptiveEntropyCoeff::new(&config)?; let initial_alpha = ae.alpha()?; diff --git a/crates/ml-ppo/src/ppo.rs b/crates/ml-ppo/src/ppo.rs index 99751b8f9..aaae71bc3 100644 --- a/crates/ml-ppo/src/ppo.rs +++ b/crates/ml-ppo/src/ppo.rs @@ -138,7 +138,7 @@ impl Default for PPOConfig { fn default() -> Self { Self { state_dim: 64, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![128, 64], value_hidden_dims: vec![256, 128, 64], policy_learning_rate: 3e-5, diff --git a/crates/ml-ppo/src/trajectories.rs b/crates/ml-ppo/src/trajectories.rs index f9af8bde0..546895c8d 100644 --- a/crates/ml-ppo/src/trajectories.rs +++ b/crates/ml-ppo/src/trajectories.rs @@ -539,7 +539,7 @@ mod tests { let advantages = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let returns = vec![0.0; 5]; let states = vec![vec![1.0]; 5]; - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let actions = vec![buy_action; 5]; let log_probs = vec![0.0; 5]; let values = vec![0.0; 5]; diff --git a/crates/ml/examples/evaluate_baseline.rs b/crates/ml/examples/evaluate_baseline.rs index 37491ea13..589f1028b 100644 --- a/crates/ml/examples/evaluate_baseline.rs +++ b/crates/ml/examples/evaluate_baseline.rs @@ -1365,7 +1365,7 @@ fn evaluate_ppo_fold_gpu( #[allow(clippy::integer_division)] let config = PPOConfig { state_dim: args.feature_dim, - num_actions: 45, + num_actions: 63, policy_hidden_dims: { let base = hp_usize(hp, "hidden_dim_base").unwrap_or(256); let align = |x: usize| x.div_ceil(8) * 8; diff --git a/crates/ml/src/benchmark/ppo_benchmark.rs b/crates/ml/src/benchmark/ppo_benchmark.rs index f714b0169..4bee45a12 100644 --- a/crates/ml/src/benchmark/ppo_benchmark.rs +++ b/crates/ml/src/benchmark/ppo_benchmark.rs @@ -108,7 +108,7 @@ impl PpoBenchmarkRunner { // Step 4: Create PPO model with batch configuration let config = PPOConfig { state_dim: 64, - num_actions: 45, // 5 exposure × 3 order × 3 urgency (FactoredAction) + num_actions: 63, // 5 exposure × 3 order × 3 urgency (FactoredAction) policy_hidden_dims: vec![128, 64], value_hidden_dims: vec![128, 64], policy_learning_rate: 3e-4, diff --git a/crates/ml/src/cuda_pipeline/signal_adapter.rs b/crates/ml/src/cuda_pipeline/signal_adapter.rs index e71af938e..ba0307299 100644 --- a/crates/ml/src/cuda_pipeline/signal_adapter.rs +++ b/crates/ml/src/cuda_pipeline/signal_adapter.rs @@ -41,19 +41,19 @@ fn load_kernels(context: &Arc) -> Result { // ── Public API ────────────────────────────────────────────────────────── -/// Aggregate PPO 45-action softmax probabilities into 5 exposure scores. +/// Aggregate PPO 63-action softmax probabilities into 7 exposure scores. /// -/// PPO uses a 45-action factored space (5 exposure x 3 order x 3 urgency). -/// This collapses the order/urgency dimensions to produce a `[batch * 5]` +/// PPO uses a 63-action factored space (7 exposure x 3 order x 3 urgency). +/// This collapses the order/urgency dimensions to produce a `[batch * 7]` /// `CudaSlice` whose per-row argmax gives the dominant exposure bucket. /// /// # Arguments -/// * `probs` - GPU-resident `[batch * 45]` softmax probabilities +/// * `probs` - GPU-resident `[batch * 63]` softmax probabilities /// * `batch` - number of rows /// * `stream` - CUDA stream for kernel launch and allocation /// /// # Returns -/// GPU-resident `CudaSlice` of shape `[batch * 5]`. +/// GPU-resident `CudaSlice` of shape `[batch * 7]`. /// /// # Errors /// Returns `MLError::ModelError` on kernel compilation or launch failure. @@ -66,7 +66,7 @@ pub fn ppo_to_exposure_scores( let kernels = load_kernels(&context)?; let mut out = stream - .alloc_zeros::(batch * 5) + .alloc_zeros::(batch * 7) .map_err(|e| MLError::ModelError(format!("alloc exposure scores: {e}")))?; let batch_i32 = batch as i32; @@ -91,15 +91,15 @@ pub fn ppo_to_exposure_scores( Ok(out) } -/// Map scalar return predictions (bps) to 5-action one-hot-ish scores. +/// Map scalar return predictions (bps) to 7-action one-hot-ish scores. /// -/// Thresholds partition the real line into five bins matching the DQN -/// exposure actions: -/// - `pred < -high` => Short100 (action 0) -/// - `-high <= pred < -low` => Short50 (action 1) -/// - `-low <= pred <= +low` => Flat (action 2) -/// - `+low < pred <= +high` => Long50 (action 3) -/// - `pred > +high` => Long100 (action 4) +/// Thresholds partition the real line into bins matching the 7-level +/// ExposureLevel enum: +/// - `pred < -high` => ShortFull (action 2) +/// - `-high <= pred < -low` => ShortHalf (action 1) +/// - `-low <= pred <= +low` => Flat (action 3) +/// - `+low < pred <= +high` => LongHalf (action 5) +/// - `pred > +high` => LongFull (action 6) /// /// # Arguments /// * `predictions` - GPU-resident `[batch]` scalar signals @@ -109,7 +109,7 @@ pub fn ppo_to_exposure_scores( /// * `stream` - CUDA stream for kernel launch and allocation /// /// # Returns -/// GPU-resident `CudaSlice` of shape `[batch * 5]` (one-hot rows). +/// GPU-resident `CudaSlice` of shape `[batch * 7]` (one-hot rows). /// /// # Errors /// Returns `MLError::ModelError` on kernel compilation or launch failure. @@ -124,7 +124,7 @@ pub fn signal_to_action_scores( let kernels = load_kernels(&context)?; let mut out = stream - .alloc_zeros::(batch * 5) + .alloc_zeros::(batch * 7) .map_err(|e| MLError::ModelError(format!("alloc action scores: {e}")))?; let batch_i32 = batch as i32; @@ -259,26 +259,26 @@ mod tests { fn test_ppo_to_exposure_scores_shape() { let stream = cuda_stream(); let batch = 4; - let uniform = vec![1.0_f32 / 45.0; batch * 45]; + let uniform = vec![1.0_f32 / 63.0; batch * 63]; let bf16_uniform: Vec = uniform.to_vec(); - let mut probs_buf = stream.alloc_zeros::(batch * 45).unwrap(); + let mut probs_buf = stream.alloc_zeros::(batch * 63).unwrap(); stream.memcpy_htod(&bf16_uniform, &mut probs_buf).unwrap(); let scores = ppo_to_exposure_scores(&probs_buf, batch, &stream).unwrap(); // Download and check shape + values - let mut host_bf16 = vec![0.0_f32; batch * 5]; - stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); // test readback + let mut host_bf16 = vec![0.0_f32; batch * 7]; + stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); stream.synchronize().unwrap(); let host: Vec = host_bf16.to_vec(); - assert_eq!(host.len(), batch * 5); - // Each of 5 bins sums 9 cells of 1/45 = 0.2 - // BF16 has limited precision -- relax tolerance + assert_eq!(host.len(), batch * 7); + // Each of 7 bins sums 9 cells of 1/63 ≈ 0.1429 + let expected = 9.0 / 63.0; for val in &host { assert!( - (*val - 0.2).abs() < 0.01, - "expected ~0.2 everywhere, got {val}" + (*val - expected).abs() < 0.01, + "expected ~{expected} everywhere, got {val}" ); } } @@ -286,45 +286,45 @@ mod tests { #[test] fn test_ppo_to_exposure_scores_argmax() { let stream = cuda_stream(); - // Put all mass on actions 36..44 => exposure bin 4 (Long100) + // Put all mass on actions 54..62 => exposure bin 6 (LongFull) let batch = 2; - let mut raw = vec![0.0_f32; batch * 45]; + let mut raw = vec![0.0_f32; batch * 63]; for b in 0..batch { - for a in 36..45 { - if let Some(slot) = raw.get_mut(b * 45 + a) { + for a in 54..63 { + if let Some(slot) = raw.get_mut(b * 63 + a) { *slot = 1.0 / 9.0; } } } let bf16_raw: Vec = raw.to_vec(); - let mut probs_buf = stream.alloc_zeros::(batch * 45).unwrap(); + let mut probs_buf = stream.alloc_zeros::(batch * 63).unwrap(); stream.memcpy_htod(&bf16_raw, &mut probs_buf).unwrap(); let scores = ppo_to_exposure_scores(&probs_buf, batch, &stream).unwrap(); - let mut host_bf16 = vec![0.0_f32; batch * 5]; - stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); // test readback + let mut host_bf16 = vec![0.0_f32; batch * 7]; + stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); stream.synchronize().unwrap(); let host: Vec = host_bf16.to_vec(); - // Each row should have argmax at index 4 (Long100) + // Each row should have argmax at index 6 (LongFull) for b in 0..batch { - let row_start = b * 5; + let row_start = b * 7; let mut best_idx = 0; let mut best_val = f32::NEG_INFINITY; - for i in 0..5 { + for i in 0..7 { let v = host[row_start + i]; if v > best_val { best_val = v; best_idx = i; } } - assert_eq!(best_idx, 4, "batch {b}: expected argmax=4, got {best_idx}"); + assert_eq!(best_idx, 6, "batch {b}: expected argmax=6 (LongFull), got {best_idx}"); } } // ── signal_to_action_scores ───────────────────────────────────────── - /// Host-side argmax over a [1, 5] row downloaded from GPU. + /// Host-side argmax over a [1, 7] row downloaded from GPU. fn host_argmax(host: &[f32]) -> usize { let mut best_idx = 0; let mut best_val = f32::NEG_INFINITY; @@ -343,8 +343,8 @@ mod tests { let mut pred_buf = stream.alloc_zeros::(1).unwrap(); stream.memcpy_htod(&bf16_pred, &mut pred_buf).unwrap(); let scores = signal_to_action_scores(&pred_buf, 1, high, low, &stream).unwrap(); - let mut host_bf16 = vec![0.0_f32; 5]; - stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); // test readback + let mut host_bf16 = vec![0.0_f32; 7]; + stream.memcpy_dtoh(&scores, &mut host_bf16).unwrap(); stream.synchronize().unwrap(); host_bf16.to_vec() } @@ -352,31 +352,31 @@ mod tests { #[test] fn test_signal_to_action_scores_strong_long() { let host = run_signal_test(20.0, 10.0, 5.0); - assert_eq!(host_argmax(&host), 4); // Long100 + assert_eq!(host_argmax(&host), 6); // LongFull } #[test] fn test_signal_to_action_scores_strong_short() { let host = run_signal_test(-20.0, 10.0, 5.0); - assert_eq!(host_argmax(&host), 0); // Short100 + assert_eq!(host_argmax(&host), 2); // ShortFull } #[test] fn test_signal_to_action_scores_flat() { let host = run_signal_test(0.0, 10.0, 5.0); - assert_eq!(host_argmax(&host), 2); // Flat + assert_eq!(host_argmax(&host), 3); // Flat } #[test] fn test_signal_to_action_scores_mild_long() { let host = run_signal_test(7.0, 10.0, 5.0); - assert_eq!(host_argmax(&host), 3); // Long50 + assert_eq!(host_argmax(&host), 5); // LongHalf } #[test] fn test_signal_to_action_scores_mild_short() { let host = run_signal_test(-7.0, 10.0, 5.0); - assert_eq!(host_argmax(&host), 1); // Short50 + assert_eq!(host_argmax(&host), 1); // ShortHalf } // ── tft_quantile_to_signal ────────────────────────────────────────── diff --git a/crates/ml/src/cuda_pipeline/signal_adapter_kernel.cu b/crates/ml/src/cuda_pipeline/signal_adapter_kernel.cu index 96c290c0e..c7c5ffe9a 100644 --- a/crates/ml/src/cuda_pipeline/signal_adapter_kernel.cu +++ b/crates/ml/src/cuda_pipeline/signal_adapter_kernel.cu @@ -4,19 +4,19 @@ * Three fused kernels replacing Candle tensor operations: * * 1. ppo_to_exposure_scores_kernel: - * Aggregates PPO 45-action softmax probs into 5 exposure scores. - * Input: [batch * 45] BF16 probs - * Output: [batch * 5] BF16 scores (sum of 9 order/urgency combos per bucket) + * Aggregates PPO 63-action softmax probs into 7 exposure scores. + * Input: [batch * 63] probs + * Output: [batch * 7] scores (sum of 9 order/urgency combos per bucket) * * 2. signal_to_action_scores_kernel: - * Maps scalar predictions to [batch, 5] one-hot-ish action scores via thresholds. - * Input: [batch] BF16 predictions - * Output: [batch * 5] BF16 scores (one-hot based on threshold buckets) + * Maps scalar predictions to [batch, 7] one-hot-ish action scores via thresholds. + * Input: [batch] predictions + * Output: [batch * 7] scores (one-hot based on threshold buckets) * * 3. tft_quantile_extract_kernel: * Extracts median signal from TFT quantile predictions. - * Input: [batch * horizon * num_quantiles] BF16 - * Output: [batch] BF16 median values (quantile index 1, horizon index 0) + * Input: [batch * horizon * num_quantiles] + * Output: [batch] median values (quantile index 1, horizon index 0) * * None of these kernels require common_device_functions.cuh (standalone). * Launch config: grid=(ceil(batch/256), 1, 1), block=(256, 1, 1). @@ -24,30 +24,30 @@ */ extern "C" __global__ void ppo_to_exposure_scores_kernel( - const float* __restrict__ probs, /* [batch * 45] softmax probs */ - float* __restrict__ out_scores, /* [batch * 5] exposure scores */ + const float* __restrict__ probs, /* [batch * 63] softmax probs */ + float* __restrict__ out_scores, /* [batch * 7] exposure scores */ int batch ) { int b = blockIdx.x * blockDim.x + threadIdx.x; if (b >= batch) return; - /* For each of 5 exposure buckets, sum over 9 order/urgency combos. - * probs layout: [batch, 45] where 45 = 5 exposure * 9 (3 order * 3 urgency) + /* For each of 7 exposure buckets, sum over 9 order/urgency combos. + * probs layout: [batch, 63] where 63 = 7 exposure * 9 (3 order * 3 urgency) * bucket e covers indices [e*9 .. e*9+8] within each batch row. */ - int base = b * 45; - for (int e = 0; e < 5; ++e) { - float sum = bf16_zero(); + int base = b * 63; + for (int e = 0; e < 7; ++e) { + float sum = 0.0f; int offset = base + e * 9; for (int j = 0; j < 9; ++j) { sum = sum + probs[offset + j]; } - out_scores[b * 5 + e] = sum; + out_scores[b * 7 + e] = sum; } } extern "C" __global__ void signal_to_action_scores_kernel( const float* __restrict__ predictions, /* [batch] scalar signals */ - float* __restrict__ out_scores, /* [batch * 5] one-hot scores */ + float* __restrict__ out_scores, /* [batch * 7] one-hot scores */ float high_threshold_bps, float low_threshold_bps, int batch @@ -56,29 +56,26 @@ extern "C" __global__ void signal_to_action_scores_kernel( if (b >= batch) return; float pred = predictions[b]; - float high_bf = bf16(high_threshold_bps); - float low_bf = bf16(low_threshold_bps); - float neg_high = bf16(-high_threshold_bps); - float neg_low = bf16(-low_threshold_bps); - int out_base = b * 5; + int out_base = b * 7; - /* Zero all 5 scores, then set the matching bucket to 1.0 */ - out_scores[out_base + 0] = bf16_zero(); - out_scores[out_base + 1] = bf16_zero(); - out_scores[out_base + 2] = bf16_zero(); - out_scores[out_base + 3] = bf16_zero(); - out_scores[out_base + 4] = bf16_zero(); + /* Zero all 7 scores, then set the matching bucket to 1.0. + * Exposure levels: 0=ShortSmall, 1=ShortHalf, 2=ShortFull, + * 3=Flat, + * 4=LongSmall, 5=LongHalf, 6=LongFull */ + for (int i = 0; i < 7; ++i) { + out_scores[out_base + i] = 0.0f; + } - if (pred < neg_high) { - out_scores[out_base + 0] = bf16_one(); /* Short100 */ - } else if (pred < neg_low) { - out_scores[out_base + 1] = bf16_one(); /* Short50 */ - } else if (pred > high_bf) { - out_scores[out_base + 4] = bf16_one(); /* Long100 */ - } else if (pred > low_bf) { - out_scores[out_base + 3] = bf16_one(); /* Long50 */ + if (pred < -high_threshold_bps) { + out_scores[out_base + 2] = 1.0f; /* ShortFull */ + } else if (pred < -low_threshold_bps) { + out_scores[out_base + 1] = 1.0f; /* ShortHalf */ + } else if (pred > high_threshold_bps) { + out_scores[out_base + 6] = 1.0f; /* LongFull */ + } else if (pred > low_threshold_bps) { + out_scores[out_base + 5] = 1.0f; /* LongHalf */ } else { - out_scores[out_base + 2] = bf16_one(); /* Flat */ + out_scores[out_base + 3] = 1.0f; /* Flat */ } } diff --git a/crates/ml/src/ensemble/adapters/ppo.rs b/crates/ml/src/ensemble/adapters/ppo.rs index 9d48218a2..15fcefa60 100644 --- a/crates/ml/src/ensemble/adapters/ppo.rs +++ b/crates/ml/src/ensemble/adapters/ppo.rs @@ -202,7 +202,7 @@ mod tests { fn test_config() -> PPOConfig { PPOConfig { state_dim: 64, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![64, 64], value_hidden_dims: vec![64, 64], ..Default::default() diff --git a/crates/ml/src/hyperopt/adapters/ppo.rs b/crates/ml/src/hyperopt/adapters/ppo.rs index 8091a97c2..f2ee2e8ea 100644 --- a/crates/ml/src/hyperopt/adapters/ppo.rs +++ b/crates/ml/src/hyperopt/adapters/ppo.rs @@ -841,7 +841,7 @@ impl HyperparameterOptimizable for PPOTrainer { // Create PPO config with trial hyperparameters let ppo_config = PPOConfig { state_dim: 48, // 42 market + 3 portfolio = 45, aligned to 48 for tensor cores - num_actions: 45, // 5 exposure × 3 order × 3 urgency (FactoredAction) + num_actions: 63, // 5 exposure × 3 order × 3 urgency (FactoredAction) policy_hidden_dims: { let base = params.hidden_dim_base; vec![base, base / 2] diff --git a/crates/ml/src/ppo/trainable_adapter.rs b/crates/ml/src/ppo/trainable_adapter.rs index f4aac0b4b..9fc00ca80 100644 --- a/crates/ml/src/ppo/trainable_adapter.rs +++ b/crates/ml/src/ppo/trainable_adapter.rs @@ -236,7 +236,7 @@ mod tests { fn test_unified_ppo_creation() -> Result<(), MLError> { let config = PPOConfig { state_dim: 16, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![32], value_hidden_dims: vec![32], policy_learning_rate: 3e-4, diff --git a/crates/ml/src/trainers/ppo.rs b/crates/ml/src/trainers/ppo.rs index 259f383ca..716ea938d 100644 --- a/crates/ml/src/trainers/ppo.rs +++ b/crates/ml/src/trainers/ppo.rs @@ -144,7 +144,7 @@ impl From for PPOConfig { PPOConfig { state_dim: 48, // 42 market + 3 portfolio = 45, aligned to 48 for tensor cores - num_actions: 45, // 5×3×3 factored action space (size × order type × duration) + num_actions: 63, // 5×3×3 factored action space (size × order type × duration) // Policy: [base, base/2] policy_hidden_dims: vec![align(base), align(base / 2)], // Value: [4*base, 3*base, 2*base, base, base/2] @@ -880,10 +880,10 @@ impl PpoTrainer { let entropy = if actions.is_empty() { 0.0_f32 } else { - let mut counts = [0_u32; 45]; + let mut counts = [0_u32; 63]; for &a in &actions { - let idx = a.clamp(0, 44) as usize; - if idx < 45 { + let idx = a.clamp(0, 62) as usize; + if idx < 63 { if let Some(c) = counts.get_mut(idx) { *c += 1; } @@ -1189,8 +1189,8 @@ mod tests { let trainer = PpoTrainer::new(params, 64, "/tmp/ppo_checkpoints", true, None).unwrap(); let hold = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); - let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); - let sell = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let buy = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); + let sell = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); // Test 1: Long position with positive return should be profitable let reward_long_up = trainer.compute_reward_pnl(&hold, 0.01, 1); // Hold with long position, market up diff --git a/crates/ml/src/validation/ppo_adapter.rs b/crates/ml/src/validation/ppo_adapter.rs index 8e891c050..3eafc429d 100644 --- a/crates/ml/src/validation/ppo_adapter.rs +++ b/crates/ml/src/validation/ppo_adapter.rs @@ -456,7 +456,7 @@ mod tests { fn make_mlp_config() -> PPOConfig { PPOConfig { state_dim: 10, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![16, 8], value_hidden_dims: vec![16, 8], batch_size: 4, diff --git a/crates/ml/tests/action_masking_smoke_test.rs b/crates/ml/tests/action_masking_smoke_test.rs index 57541eb9c..94522d7e8 100644 --- a/crates/ml/tests/action_masking_smoke_test.rs +++ b/crates/ml/tests/action_masking_smoke_test.rs @@ -75,82 +75,72 @@ //! Smoke tests for action masking functionality (Wave 9 Agent 2) //! //! Validates position limit enforcement via action masking. -//! With branching DQN, the mask has 5 elements (one per exposure level): -//! [Short100, Short50, Flat, Long50, Long100] +//! With 7-level ExposureLevel, the mask has 7 elements: +//! [ShortSmall, ShortHalf, ShortFull, Flat, LongSmall, LongHalf, LongFull] use ml::dqn::action_space::{get_valid_action_mask, ExposureLevel, FactoredAction}; #[test] fn test_action_masking_at_neutral_position() { - // At position 0.0 with max_position=2.0, all 5 exposure levels valid + // At position 0.0 with max_position=2.0, all 7 exposure levels valid let mask = get_valid_action_mask(0.0, 2.0); - assert_eq!(mask.len(), 5, "Mask should have 5 elements (one per exposure level)"); + assert_eq!(mask.len(), 7, "Mask should have 7 elements (one per exposure level)"); assert_eq!( mask.iter().filter(|&&v| v).count(), - 5, + 7, "All exposure levels should be valid at position 0.0 with max_position=2.0" ); } #[test] fn test_action_masking_very_restrictive_limit() { - // With max_position=0.6, only Flat and ±50% exposures should be valid + // With max_position=0.6, ShortFull and LongFull should be masked (|1.0| > 0.6) let mask = get_valid_action_mask(0.0, 0.6); let valid_count = mask.iter().filter(|&&v| v).count(); assert_eq!( - valid_count, 3, - "Only Flat, Short50, and Long50 should be valid (3/5 exposures)" + valid_count, 5, + "ShortSmall, ShortHalf, Flat, LongSmall, LongHalf valid (5/7 exposures)" ); - // Short100 (index 0) invalid: |exposure|=1.0 > 0.6 - assert!(!mask[0], "Short100 should be INVALID (exposure=-1.0 > 0.6)"); - - // Short50 (index 1) valid: |exposure|=0.5 <= 0.6 - assert!(mask[1], "Short50 should be VALID (exposure=-0.5 <= 0.6)"); - - // Flat (index 2) valid: |exposure|=0.0 <= 0.6 - assert!(mask[2], "Flat should be VALID (exposure=0.0 <= 0.6)"); - - // Long50 (index 3) valid: |exposure|=0.5 <= 0.6 - assert!(mask[3], "Long50 should be VALID (exposure=+0.5 <= 0.6)"); - - // Long100 (index 4) invalid: |exposure|=1.0 > 0.6 - assert!(!mask[4], "Long100 should be INVALID (exposure=+1.0 > 0.6)"); + assert!(mask[0], "ShortSmall (0.25) should be VALID"); + assert!(mask[1], "ShortHalf (0.50) should be VALID"); + assert!(!mask[2], "ShortFull (1.0) should be INVALID"); + assert!(mask[3], "Flat (0.0) should be VALID"); + assert!(mask[4], "LongSmall (0.25) should be VALID"); + assert!(mask[5], "LongHalf (0.50) should be VALID"); + assert!(!mask[6], "LongFull (1.0) should be INVALID"); } #[test] fn test_action_masking_preserves_all_action_variants() { - // With masking at max_position=1.0, all 5 exposure levels valid + // With masking at max_position=1.0, all 7 exposure levels valid let mask = get_valid_action_mask(0.0, 1.0); - assert_eq!(mask.len(), 5); - - assert!(mask[0], "Short100 should be valid"); - assert!(mask[1], "Short50 should be valid"); - assert!(mask[2], "Flat should be valid"); - assert!(mask[3], "Long50 should be valid"); - assert!(mask[4], "Long100 should be valid"); + assert_eq!(mask.len(), 7); + assert!(mask.iter().all(|&v| v), "All 7 exposures should be valid at max_position=1.0"); } #[test] fn test_action_masking_index_mapping_correctness() { let mask = get_valid_action_mask(0.0, 0.6); - assert_eq!(mask.len(), 5); + assert_eq!(mask.len(), 7); - // Exposure order: Short100, Short50, Flat, Long50, Long100 - assert!(!mask[0], "Short100 (idx 0) should be masked"); - assert!(mask[1], "Short50 (idx 1) should be valid"); - assert!(mask[2], "Flat (idx 2) should be valid"); - assert!(mask[3], "Long50 (idx 3) should be valid"); - assert!(!mask[4], "Long100 (idx 4) should be masked"); + // ShortFull (idx 2) and LongFull (idx 6) should be masked + assert!(mask[0], "ShortSmall should be valid"); + assert!(mask[1], "ShortHalf should be valid"); + assert!(!mask[2], "ShortFull should be masked"); + assert!(mask[3], "Flat should be valid"); + assert!(mask[4], "LongSmall should be valid"); + assert!(mask[5], "LongHalf should be valid"); + assert!(!mask[6], "LongFull should be masked"); - // Verify FactoredAction mapping still works for the 45-action space + // Verify FactoredAction mapping still works for the 63-action space let action_0 = FactoredAction::from_index(0).unwrap(); - assert_eq!(action_0.exposure, ExposureLevel::Short100); + assert_eq!(action_0.exposure, ExposureLevel::ShortSmall); - let action_18 = FactoredAction::from_index(18).unwrap(); - assert_eq!(action_18.exposure, ExposureLevel::Flat); + let action_27 = FactoredAction::from_index(27).unwrap(); + assert_eq!(action_27.exposure, ExposureLevel::Flat); } #[test] @@ -158,12 +148,12 @@ fn test_action_masking_boundary_conditions() { // At max_position=1.0, all exposure levels valid (max |exp| = 1.0 <= 1.0) let mask = get_valid_action_mask(0.0, 1.0); let valid_count = mask.iter().filter(|&&v| v).count(); - assert_eq!(valid_count, 5, "All exposures valid when max |exposure| equals max_position"); + assert_eq!(valid_count, 7, "All exposures valid when max |exposure| equals max_position"); - // At max_position=0.99, Short100/Long100 masked (|1.0| > 0.99) + // At max_position=0.99, ShortFull/LongFull masked (|1.0| > 0.99) let mask_below = get_valid_action_mask(0.0, 0.99); let valid_below = mask_below.iter().filter(|&&v| v).count(); - assert_eq!(valid_below, 3, "Only 3 exposures valid when max_position < 1.0"); + assert_eq!(valid_below, 5, "Only 5 exposures valid when max_position < 1.0"); } #[test] @@ -172,7 +162,7 @@ fn test_action_masking_flat_always_valid() { for max_pos in &[0.1, 0.5, 1.0, 2.0, 10.0] { let mask = get_valid_action_mask(0.0, *max_pos); assert!( - mask[2], // Flat is index 2 in the 5-element mask + mask[3], // Flat is index 3 in the 7-element mask "Flat should always be valid (max_position={})", max_pos ); @@ -187,9 +177,11 @@ fn test_action_masking_edge_case_zero_max_position() { let valid_count = mask.iter().filter(|&&v| v).count(); assert_eq!(valid_count, 1, "Only Flat should be valid when max_position=0.0"); - assert!(!mask[0], "Short100 invalid at max_position=0.0"); - assert!(!mask[1], "Short50 invalid at max_position=0.0"); - assert!(mask[2], "Flat valid at max_position=0.0"); - assert!(!mask[3], "Long50 invalid at max_position=0.0"); - assert!(!mask[4], "Long100 invalid at max_position=0.0"); + assert!(!mask[0], "ShortSmall invalid at max_position=0.0"); + assert!(!mask[1], "ShortHalf invalid at max_position=0.0"); + assert!(!mask[2], "ShortFull invalid at max_position=0.0"); + assert!(mask[3], "Flat valid at max_position=0.0"); + assert!(!mask[4], "LongSmall invalid at max_position=0.0"); + assert!(!mask[5], "LongHalf invalid at max_position=0.0"); + assert!(!mask[6], "LongFull invalid at max_position=0.0"); } diff --git a/crates/ml/tests/bug16_portfolio_features_test.rs b/crates/ml/tests/bug16_portfolio_features_test.rs index 024345a42..63cc871cd 100644 --- a/crates/ml/tests/bug16_portfolio_features_test.rs +++ b/crates/ml/tests/bug16_portfolio_features_test.rs @@ -95,12 +95,12 @@ use tracing::info; /// Helper: Create a BUY action (Long100) fn create_buy_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal) } /// Helper: Create a SELL action (Short100) fn create_sell_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal) } /// Helper: Create a HOLD action (Flat) diff --git a/crates/ml/tests/bug18_cash_reserve_solvency_test.rs b/crates/ml/tests/bug18_cash_reserve_solvency_test.rs index 3aa5a42f7..f8446cc77 100644 --- a/crates/ml/tests/bug18_cash_reserve_solvency_test.rs +++ b/crates/ml/tests/bug18_cash_reserve_solvency_test.rs @@ -109,7 +109,7 @@ fn test_cash_reserve_zero_allows_full_trading() { assert_eq!(tracker.current_position(), 0.0, "Initial position should be 0.0"); // Execute: Long 2.0 @ $5,000/contract (Long100 × max_position=2.0) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let price = 5000.0; // $5,000 per contract let max_position = 2.0; // Target: 2.0 contracts @@ -169,7 +169,7 @@ fn test_cash_reserve_10_percent_reasonable() { assert_eq!(tracker.current_position(), 0.0); // Execute: Long 2.0 @ $5,000/contract - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let price = 5000.0; let max_position = 2.0; @@ -221,7 +221,7 @@ fn test_high_cash_reserve_prevents_trading() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, cash_reserve); // Execute: Long 2.0 @ $5,000/contract - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let price = 5000.0; let max_position = 2.0; @@ -275,7 +275,7 @@ fn test_position_reversals_maintain_solvency() { let max_position = 2.0; // Reversal 1: Long 2.0 @ $5,000 - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, 5000.0, max_position); let cash_after_long1 = tracker.cash_balance(); @@ -296,7 +296,7 @@ fn test_position_reversals_maintain_solvency() { ); // Reversal 2: Close and reverse to Short -2.0 @ $5,100 - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, new_price, max_position); let cash_after_short = tracker.cash_balance(); @@ -371,7 +371,7 @@ fn test_short_positions_generate_cash() { assert_eq!(initial_cash, 100_000.0); // Execute: Short -2.0 @ $5,000/contract - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); let price = 5000.0; let max_position = 2.0; @@ -426,7 +426,7 @@ fn test_cash_reserve_boundary_zero_point_one() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, cash_reserve); // Execute: Long 2.0 @ $5,000 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 2.0); let position = tracker.current_position(); @@ -458,7 +458,7 @@ fn test_cash_reserve_boundary_fifty_percent() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, cash_reserve); // Execute: Long 2.0 @ $5,000 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 2.0); // NOTE: Current implementation does NOT enforce reserve constraint diff --git a/crates/ml/tests/bug21_bug22_bug23_compilation_fixes_test.rs b/crates/ml/tests/bug21_bug22_bug23_compilation_fixes_test.rs index c8a0cda1c..0bc61ea1e 100644 --- a/crates/ml/tests/bug21_bug22_bug23_compilation_fixes_test.rs +++ b/crates/ml/tests/bug21_bug22_bug23_compilation_fixes_test.rs @@ -184,7 +184,7 @@ fn test_unrealized_pnl_with_current_price() { // Open a long position at $5000 use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency}; - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // 10 contracts at $5000 // Verify position opened @@ -221,7 +221,7 @@ fn test_portfolio_tracker_comprehensive_integration() { use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency}; // Test 1: Open long position - let long_action = FactoredAction::new(ExposureLevel::Long50, OrderType::LimitMaker, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongSmall, OrderType::LimitMaker, Urgency::Normal); tracker.execute_action(long_action, 5000.0, 10.0); // 5 contracts (50% of 10 max) assert_eq!(tracker.current_position(), 5.0); diff --git a/crates/ml/tests/cash_accounting_fix_test.rs b/crates/ml/tests/cash_accounting_fix_test.rs index 73685dea9..03dc84d26 100644 --- a/crates/ml/tests/cash_accounting_fix_test.rs +++ b/crates/ml/tests/cash_accounting_fix_test.rs @@ -83,7 +83,7 @@ mod cash_accounting_tests { let initial_cash = tracker.cash_balance(); // Buy 1 contract at $5,600 (go from 0 to +1 position) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5600.0, 1.0); let final_cash = tracker.cash_balance(); @@ -107,7 +107,7 @@ mod cash_accounting_tests { let initial_cash = tracker.cash_balance(); // Sell 1 contract at $5,600 (go from 0 to -1 position) - let action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5600.0, 1.0); let final_cash = tracker.cash_balance(); @@ -130,7 +130,7 @@ mod cash_accounting_tests { let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0); // First, buy 1 contract - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(buy_action, 5600.0, 1.0); let cash_after_buy = tracker.cash_balance(); @@ -155,7 +155,7 @@ mod cash_accounting_tests { // Execute 10 round-trip trades at same price for _ in 0..10 { // Buy - let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(buy, 5600.0, 1.0); // Sell diff --git a/crates/ml/tests/cash_reserve_requirement_test.rs b/crates/ml/tests/cash_reserve_requirement_test.rs index 61a1565ad..ea8d34aae 100644 --- a/crates/ml/tests/cash_reserve_requirement_test.rs +++ b/crates/ml/tests/cash_reserve_requirement_test.rs @@ -112,7 +112,7 @@ fn test_no_reserve_baseline() { // Action: BUY Long100 at $5000 with max_position=10 contracts (use smaller max for simpler version) // Target: 10 contracts (Long100 = 1.0 × 10) // Cost: 10 contracts × $5000 = $50,000 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Expected: Trade executes (0% reserve = no constraint) @@ -135,7 +135,7 @@ fn test_conservative_reserve_buy_accepted() { // Action: BUY Long50 at $5000 (0.5 contracts clamped to 0.5) // Cost: 5.0 × $5000 = $25,000 // Cash after: $100K - $25K = $75K (> $3.75K reserve) ✅ - let action = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Expected: Trade executes @@ -157,7 +157,7 @@ fn test_standard_reserve_buy_accepted() { // Action: BUY Long50 at $5000 (0.5 × 10 = 5.0 position) // Cost: 5.0 × $5000 = $25,000 // Cash after: $100K - $25K = $75K (> $7.5K reserve) ✅ - let action = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Expected: Trade executes @@ -181,12 +181,12 @@ fn test_standard_reserve_buy_rejected() { // Long100 (1.0) × max_position=10 = 10.0 position // Cost: 10.0 × $5000 = $50,000 // Cash after: $100K - $50K = $50K (portfolio value ~$50K, reserve ~$5K) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Step 2: Attempt second buy which should violate reserve // This would cost another $50K, leaving cash at $0 (< $5K reserve) ❌ - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Expected: Trade REJECTED or REDUCED (cash should not go below reserve) @@ -217,12 +217,12 @@ fn test_aggressive_reserve_buy_rejected() { // Step 1: Execute first buy // Cost: 10.0 × $5000 = $50,000 // Cash after: $50K (portfolio value ~$50K, reserve ~$7.5K) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); // Step 2: Attempt second buy // This would cost another $50K, leaving cash at $0 (< $7.5K reserve) ❌ - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); let cash_after = tracker.cash_balance(); @@ -246,14 +246,14 @@ fn test_reserve_sell_always_allowed() { let mut tracker = create_tracker_with_reserve(10.0); // Step 1: Build a long position - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(buy_action, 5000.0, 10.0); let cash_before_sell = tracker.cash_balance(); let position_before_sell = tracker.current_position(); // Step 2: Execute SELL (should always work, regardless of cash level) - let sell_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let sell_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(sell_action, 5000.0, 10.0); // Expected: SELL executes (position changes, cash increases) @@ -283,7 +283,7 @@ fn test_reserve_dynamic_portfolio_growth() { // Initial: $100K cash, reserve = $10K // Step 1: BUY Long50 at $5000 - let action = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); let pv_after_buy = tracker.total_value(5000.0); @@ -313,7 +313,7 @@ fn test_reserve_dynamic_portfolio_shrinkage() { let mut tracker = create_tracker_with_reserve(10.0); // Step 1: BUY Long100 at $5000 - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(buy_action, 5000.0, 10.0); let pv_at_5k = tracker.total_value(5000.0); @@ -344,7 +344,7 @@ fn test_reserve_with_transaction_costs() { // Action: BUY with Market order (higher fee 0.15% vs LimitMaker 0.05%) // Cost: 10.0 × $5000 = $50,000 - let market_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let market_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(market_action, 5000.0, 10.0); let cash_after_market = tracker.cash_balance(); @@ -378,14 +378,14 @@ fn test_reserve_edge_case_exact_boundary() { // Drain cash to just above reserve // First buy: $50K cost, leaves $50K cash - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); let portfolio_value = tracker.total_value(5000.0); let reserve_required = portfolio_value * 0.10; // Attempt second trade (would drain $50K → $0, violating reserve) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 5000.0, 10.0); let cash_final = tracker.cash_balance(); diff --git a/crates/ml/tests/contract_multiplier_test.rs b/crates/ml/tests/contract_multiplier_test.rs index 0ba60b35a..089e24233 100644 --- a/crates/ml/tests/contract_multiplier_test.rs +++ b/crates/ml/tests/contract_multiplier_test.rs @@ -98,7 +98,7 @@ fn test_es_multiplier_50_per_point() { let max_position = 10.0; // Buy 1 contract (Long100 with MAX_POSITION_CONTRACTS=1.0 limit) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let features = tracker.get_raw_portfolio_features(price); @@ -126,7 +126,7 @@ fn test_nq_multiplier_20_per_point() { let price = 18000.0; let max_position = 10.0; - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let features = tracker.get_raw_portfolio_features(price); @@ -154,7 +154,7 @@ fn test_zn_multiplier_1000_per_point() { let price = 110.0; let max_position = 10.0; - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let features = tracker.get_raw_portfolio_features(price); @@ -183,7 +183,7 @@ fn test_es_round_trip_with_multiplier() { let max_position = 10.0; // Buy 1 contract @ 5600 - let buy_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let buy_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(buy_action, buy_price, max_position); // Sell 1 contract @ 5650 (50 point profit) @@ -221,7 +221,7 @@ fn test_cash_accounting_with_multiplier() { let initial_cash = tracker.cash_balance(); // Buy 1 contract @ 5600 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let final_cash = tracker.cash_balance(); @@ -250,7 +250,7 @@ fn test_no_multiplier_default() { let price = 5600.0; let max_position = 10.0; - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let features = tracker.get_raw_portfolio_features(price); @@ -281,7 +281,7 @@ fn test_multiple_contracts_with_multiplier() { let price = 5600.0; let max_position = 10.0; // Request 10 contracts - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); let features = tracker.get_raw_portfolio_features(price); @@ -305,7 +305,7 @@ fn test_short_position_with_multiplier() { let max_position = 10.0; // Short 1 contract @ 5600 - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, short_price, max_position); // Cover @ 5550 (50 point profit) @@ -341,7 +341,7 @@ fn test_unrealized_pnl_with_multiplier() { let max_position = 10.0; // Buy 1 contract @ 5600 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, entry_price, max_position); // Calculate unrealized P&L at 5650 diff --git a/crates/ml/tests/debug_position_delta_test.rs b/crates/ml/tests/debug_position_delta_test.rs index b594685dd..7c80ef2a9 100644 --- a/crates/ml/tests/debug_position_delta_test.rs +++ b/crates/ml/tests/debug_position_delta_test.rs @@ -92,7 +92,7 @@ fn test_position_delta_sign_when_buying() { // Execute: Go from FLAT (0) to LONG (1.0 contract) let action = FactoredAction::new( - ExposureLevel::Long100, // target_exposure = +1.0 + ExposureLevel::LongFull, // target_exposure = +1.0 OrderType::Market, Urgency::Normal ); @@ -164,7 +164,7 @@ fn test_position_delta_sign_when_selling() { info!("TEST 2: GOING SHORT — Initial: FLAT (0 contracts) to Short100 (-1.0 contracts)"); let action = FactoredAction::new( - ExposureLevel::Short100, // target_exposure = -1.0 + ExposureLevel::ShortFull, // target_exposure = -1.0 OrderType::Market, Urgency::Normal ); diff --git a/crates/ml/tests/dqn_action_masking_integration_test.rs b/crates/ml/tests/dqn_action_masking_integration_test.rs index f76bfc5eb..8559ef86b 100644 --- a/crates/ml/tests/dqn_action_masking_integration_test.rs +++ b/crates/ml/tests/dqn_action_masking_integration_test.rs @@ -331,7 +331,7 @@ fn test_action_mapping_correctness() { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.exposure, - ExposureLevel::Short100, + ExposureLevel::ShortSmall, "Action {} should have Short100 exposure", idx ); @@ -342,7 +342,7 @@ fn test_action_mapping_correctness() { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.exposure, - ExposureLevel::Short50, + ExposureLevel::ShortFull, "Action {} should have Short50 exposure", idx ); @@ -364,7 +364,7 @@ fn test_action_mapping_correctness() { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.exposure, - ExposureLevel::Long50, + ExposureLevel::LongSmall, "Action {} should have Long50 exposure", idx ); @@ -375,7 +375,7 @@ fn test_action_mapping_correctness() { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!( action.exposure, - ExposureLevel::Long100, + ExposureLevel::LongFull, "Action {} should have Long100 exposure", idx ); diff --git a/crates/ml/tests/dqn_action_position_sign_convention_test.rs b/crates/ml/tests/dqn_action_position_sign_convention_test.rs index 83202f9ce..d9b6f156e 100644 --- a/crates/ml/tests/dqn_action_position_sign_convention_test.rs +++ b/crates/ml/tests/dqn_action_position_sign_convention_test.rs @@ -99,11 +99,13 @@ fn test_all_45_actions_sign_convention() { // Expected exposure values for each ExposureLevel let expected_exposures = [ - (ExposureLevel::Long100, 1.0, "Long100"), - (ExposureLevel::Long50, 0.5, "Long50"), + (ExposureLevel::LongFull, 1.0, "LongFull"), + (ExposureLevel::LongHalf, 0.5, "LongHalf"), + (ExposureLevel::LongSmall, 0.25, "LongSmall"), (ExposureLevel::Flat, 0.0, "Flat"), - (ExposureLevel::Short50, -0.5, "Short50"), - (ExposureLevel::Short100, -1.0, "Short100"), + (ExposureLevel::ShortSmall, -0.25, "ShortSmall"), + (ExposureLevel::ShortHalf, -0.5, "ShortHalf"), + (ExposureLevel::ShortFull, -1.0, "ShortFull"), ]; let order_types = [ @@ -326,18 +328,18 @@ fn test_position_transitions() { let mut portfolio = PortfolioTracker::new(10000.0, 0.01, 0.0); // Transition 1: Flat → Long100 - let action1 = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action1 = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); portfolio.execute_action(action1, price as f32, max_position as f32); let pos1 = portfolio.current_position() as f64; info!(pos1, "Flat to Long100 position"); assert_eq!(pos1, 10.0, "Should be at +10.0 after Long100"); - // Transition 2: Long100 → Short100 - let action2 = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + // Transition 2: LongFull → ShortFull + let action2 = FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal); portfolio.execute_action(action2, price as f32, max_position as f32); let pos2 = portfolio.current_position() as f64; - info!(pos2, "Long100 to Short100 position"); - assert_eq!(pos2, -10.0, "Should be at -10.0 after Short100"); + info!(pos2, "LongFull to ShortFull position"); + assert_eq!(pos2, -10.0, "Should be at -10.0 after ShortFull"); // Transition 3: Short100 → Flat let action3 = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal); @@ -346,19 +348,19 @@ fn test_position_transitions() { info!(pos3, "Short100 to Flat position"); assert_eq!(pos3, 0.0, "Should be at 0.0 after Flat"); - // Transition 4: Flat → Short50 - let action4 = FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal); + // Transition 4: Flat → ShortFull + let action4 = FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal); portfolio.execute_action(action4, price as f32, max_position as f32); let pos4 = portfolio.current_position() as f64; - info!(pos4, "Flat to Short50 position"); - assert_eq!(pos4, -5.0, "Should be at -5.0 after Short50"); + info!(pos4, "Flat to ShortFull position"); + assert_eq!(pos4, -10.0, "Should be at -10.0 after ShortFull"); - // Transition 5: Short50 → Long50 - let action5 = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + // Transition 5: ShortFull → LongHalf + let action5 = FactoredAction::new(ExposureLevel::LongHalf, OrderType::Market, Urgency::Normal); portfolio.execute_action(action5, price as f32, max_position as f32); let pos5 = portfolio.current_position() as f64; - info!(pos5, "Short50 to Long50 position"); - assert_eq!(pos5, 5.0, "Should be at +5.0 after Long50"); + info!(pos5, "ShortFull to LongHalf position"); + assert_eq!(pos5, 5.0, "Should be at +5.0 after LongHalf"); info!("All position transitions validated successfully"); } diff --git a/crates/ml/tests/dqn_gradient_collapse_root_cause_test.rs b/crates/ml/tests/dqn_gradient_collapse_root_cause_test.rs index 799320ad7..dfd259861 100644 --- a/crates/ml/tests/dqn_gradient_collapse_root_cause_test.rs +++ b/crates/ml/tests/dqn_gradient_collapse_root_cause_test.rs @@ -151,7 +151,7 @@ fn test_network_output_dtype() -> Result<(), MLError> { let config = DistributionalDuelingConfig { state_dim: 54, - num_actions: 45, + num_actions: 63, num_atoms: 51, shared_hidden_dims: vec![128, 64], value_hidden_dim: 64, diff --git a/crates/ml/tests/dqn_gradient_dtype_simple_test.rs b/crates/ml/tests/dqn_gradient_dtype_simple_test.rs index 273400fba..74cf25c76 100644 --- a/crates/ml/tests/dqn_gradient_dtype_simple_test.rs +++ b/crates/ml/tests/dqn_gradient_dtype_simple_test.rs @@ -101,7 +101,7 @@ fn test_network_outputs_f32_naturally() -> Result<(), MLError> { // Create network config matching production let config = DistributionalDuelingConfig { state_dim: 54, - num_actions: 45, + num_actions: 63, num_atoms: 51, shared_hidden_dims: vec![256, 128], value_hidden_dim: 128, @@ -174,7 +174,7 @@ fn test_if_dtype_conversion_line_1087_is_necessary() -> Result<(), MLError> { // Recreate exact code path from dqn.rs lines 1079-1087 let config = DistributionalDuelingConfig { state_dim: 54, - num_actions: 45, + num_actions: 63, num_atoms: 51, shared_hidden_dims: vec![256, 128], value_hidden_dim: 128, diff --git a/crates/ml/tests/dqn_kelly_regime_integration_test.rs b/crates/ml/tests/dqn_kelly_regime_integration_test.rs index 70dde20a7..3d73ae08b 100644 --- a/crates/ml/tests/dqn_kelly_regime_integration_test.rs +++ b/crates/ml/tests/dqn_kelly_regime_integration_test.rs @@ -260,7 +260,7 @@ fn test_kelly_scales_position_size() { // Test 1: Kelly = 0.5 (half Kelly) let mut tracker_half = PortfolioTracker::new(initial_capital, 0.0001, 0.0); let long_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); @@ -374,7 +374,7 @@ fn test_kelly_applied_in_backtest() { for &price in &price_sequence { let long_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); @@ -389,7 +389,7 @@ fn test_kelly_applied_in_backtest() { for &price in &price_sequence { let long_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); diff --git a/crates/ml/tests/dqn_pnl_calculation_tests.rs b/crates/ml/tests/dqn_pnl_calculation_tests.rs index 3ecef1d25..0ad5c62f4 100644 --- a/crates/ml/tests/dqn_pnl_calculation_tests.rs +++ b/crates/ml/tests/dqn_pnl_calculation_tests.rs @@ -109,7 +109,7 @@ fn test_pnl_calculation_realistic_sizing() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // WHEN: Execute Long100 (full long position) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); // THEN: Position should be ~1.724 units (NOT 1.0 or 0.0345) @@ -147,7 +147,7 @@ fn test_pnl_range_realistic() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // WHEN: Go long at $5,800 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price_entry, max_position); // AND: Price increases 10% to $6,380 @@ -186,7 +186,7 @@ fn test_old_bug_wrong_pnl() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // WHEN: Go long at $5,800 with 1.0 unit - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price_entry, max_position_wrong); // AND: Price increases 10% to $6,380 @@ -244,7 +244,7 @@ fn test_short_position_sizing() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // WHEN: Execute Short100 (full short position) - let action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); // THEN: Position should be -1.724 units (negative for short) @@ -275,7 +275,7 @@ fn test_trainer_initial_capital() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 1.0); // WHEN: Execute Long100 at $5,800 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, price, max_position); // THEN: Position should be ~17.24 units @@ -312,7 +312,7 @@ fn test_last_price_updated() { let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0); // WHEN: Execute action at $5,800 - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let price = 5_800.0; let max_position = 100_000.0 / price; tracker.execute_action(action, price, max_position); @@ -350,15 +350,15 @@ fn test_epoch_pnl_range() { // Simulate 10 trades across price range let actions = vec![ - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), - FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal), FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), - FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal), - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal), FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), - FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal), FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), - FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal), FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal), ]; diff --git a/crates/ml/tests/dqn_transaction_cost_bug2_test.rs b/crates/ml/tests/dqn_transaction_cost_bug2_test.rs index 49b2898b5..430b3492a 100644 --- a/crates/ml/tests/dqn_transaction_cost_bug2_test.rs +++ b/crates/ml/tests/dqn_transaction_cost_bug2_test.rs @@ -207,21 +207,21 @@ fn test_hyperopt_search_space_reduced() { fn test_factored_action_transaction_costs() { // Market order (most expensive) let market_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); // Limit maker order (cheapest) let limit_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::LimitMaker, Urgency::Patient, ); // IoC order (middle) let ioc_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::IoC, Urgency::Normal, ); diff --git a/crates/ml/tests/dqn_transaction_costs_test.rs b/crates/ml/tests/dqn_transaction_costs_test.rs index b8608b2c2..06b8fd41b 100644 --- a/crates/ml/tests/dqn_transaction_costs_test.rs +++ b/crates/ml/tests/dqn_transaction_costs_test.rs @@ -105,7 +105,7 @@ async fn test_order_type_transaction_costs() { // Market order: 0.15% (highest cost) let market_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); @@ -117,7 +117,7 @@ async fn test_order_type_transaction_costs() { // LimitMaker order: 0.05% (lowest cost) let limit_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::LimitMaker, Urgency::Patient, ); @@ -128,7 +128,7 @@ async fn test_order_type_transaction_costs() { ); // IoC order: 0.10% (medium cost) - let ioc_action = FactoredAction::new(ExposureLevel::Long100, OrderType::IoC, Urgency::Normal); + let ioc_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::IoC, Urgency::Normal); let ioc_cost = ioc_action.calculate_transaction_cost(trade_value); assert_eq!(ioc_cost, 10.0, "IoC order should cost $10 (0.10% of $10k)"); } @@ -140,11 +140,11 @@ async fn test_market_costs_twice_limitmaker() { let trade_value = 5_000.0; let market_action = - FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); let market_cost = market_action.calculate_transaction_cost(trade_value); let limit_action = FactoredAction::new( - ExposureLevel::Long50, + ExposureLevel::LongSmall, OrderType::LimitMaker, Urgency::Normal, ); @@ -182,12 +182,12 @@ fn test_exposure_scaling_transaction_costs() { let position_size = 1.0; // Short100 (-100% exposure) - let short100 = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short100 = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); let short100_value = entry_price * position_size * short100.target_exposure().abs(); let short100_cost = short100.calculate_transaction_cost(short100_value); // Short50 (-50% exposure) - let short50 = FactoredAction::new(ExposureLevel::Short50, OrderType::Market, Urgency::Normal); + let short50 = FactoredAction::new(ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal); let short50_value = entry_price * position_size * short50.target_exposure().abs(); let short50_cost = short50.calculate_transaction_cost(short50_value); @@ -197,12 +197,12 @@ fn test_exposure_scaling_transaction_costs() { let flat_cost = flat.calculate_transaction_cost(flat_value); // Long50 (+50% exposure) - let long50 = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal); + let long50 = FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal); let long50_value = entry_price * position_size * long50.target_exposure().abs(); let long50_cost = long50.calculate_transaction_cost(long50_value); // Long100 (+100% exposure) - let long100 = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long100 = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let long100_value = entry_price * position_size * long100.target_exposure().abs(); let long100_cost = long100.calculate_transaction_cost(long100_value); @@ -238,10 +238,10 @@ fn test_urgency_does_not_affect_transaction_costs() { let trade_value = 8_000.0; - let patient = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Patient); - let normal = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let patient = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Patient); + let normal = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let aggressive = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Aggressive, ); @@ -267,7 +267,7 @@ fn test_all_45_actions_have_valid_transaction_costs() { let trade_value = 10_000.0; - for action_idx in 0..45 { + for action_idx in 0..63 { let action = FactoredAction::from_index(action_idx) .expect(&format!("Action index {} should be valid", action_idx)); @@ -346,7 +346,7 @@ fn test_cost_calculation_matches_documentation() { let trade_value = 100_000.0; // $100k trade let market_action = - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); assert_eq!( market_action.calculate_transaction_cost(trade_value), 150.0, @@ -354,7 +354,7 @@ fn test_cost_calculation_matches_documentation() { ); let limit_action = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::LimitMaker, Urgency::Normal, ); @@ -364,7 +364,7 @@ fn test_cost_calculation_matches_documentation() { "LimitMaker: 0.05% of $100k = $50" ); - let ioc_action = FactoredAction::new(ExposureLevel::Long100, OrderType::IoC, Urgency::Normal); + let ioc_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::IoC, Urgency::Normal); assert_eq!( ioc_action.calculate_transaction_cost(trade_value), 100.0, @@ -379,7 +379,7 @@ fn test_transaction_cost_precision() { let small_trade = 100.0; // $100 trade let market_action = - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let cost = market_action.calculate_transaction_cost(small_trade); // 0.15% of $100 = $0.15 diff --git a/crates/ml/tests/ensemble_inference_integration_test.rs b/crates/ml/tests/ensemble_inference_integration_test.rs index 07e6c1c6c..cbfce0504 100644 --- a/crates/ml/tests/ensemble_inference_integration_test.rs +++ b/crates/ml/tests/ensemble_inference_integration_test.rs @@ -94,7 +94,7 @@ use ml::tft::TFTConfig; fn small_dqn_config() -> DQNConfig { DQNConfig { state_dim: 51, - num_actions: 45, + num_actions: 63, hidden_dims: vec![64, 64], ..Default::default() } @@ -104,7 +104,7 @@ fn small_dqn_config() -> DQNConfig { fn small_ppo_config() -> PPOConfig { PPOConfig { state_dim: 64, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![64, 64], value_hidden_dims: vec![64, 64], ..Default::default() diff --git a/crates/ml/tests/partial_reversal_support_test.rs b/crates/ml/tests/partial_reversal_support_test.rs index 44dbfa4a3..e9c7ed907 100644 --- a/crates/ml/tests/partial_reversal_support_test.rs +++ b/crates/ml/tests/partial_reversal_support_test.rs @@ -120,14 +120,14 @@ fn test_full_reversal_sufficient_cash() { let price = 5_600.0; // ES futures typical price // Step 1: Open short position (position = -1.0 contract) - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let position_after_short = tracker.current_position(); assert_eq!(position_after_short, -1.0, "Should have -1.0 short position"); // Step 2: Execute Long100 reversal with sufficient cash - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); @@ -156,13 +156,13 @@ fn test_short_to_flat_only() { let mut tracker = PortfolioTracker::new(starting_cash, 0.0001, 0.0); // Open short position first - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); assert_eq!(tracker.current_position(), -1.0, "Should have -1.0 short position"); // Now attempt Long100 reversal - should partially fill to Flat only - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // This test validates that with limited cash, the reversal stops at Flat (0.0) // The current implementation may reject entirely - we'll see what happens @@ -191,7 +191,7 @@ fn test_short_to_partial_long() { let price = 5_600.0; // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); assert_eq!(tracker.current_position(), -1.0, "Should have -1.0 short position"); @@ -206,7 +206,7 @@ fn test_short_to_partial_long() { // With partial reversal support, if we have cash for Phase 1 (close short) + partial Phase 2, // we should end up with a position between 0.0 and +1.0 (e.g., +0.3) - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); @@ -227,13 +227,13 @@ fn test_long_to_flat_only() { let price = 5_600.0; // Open long position - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); assert_eq!(tracker.current_position(), 1.0, "Should have +1.0 long position"); // Attempt Short100 reversal with limited cash - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let final_position = tracker.current_position(); @@ -255,13 +255,13 @@ fn test_long_to_partial_short() { let price = 5_600.0; // Open long position - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); assert_eq!(tracker.current_position(), 1.0, "Should have +1.0 long position"); // Attempt Short100 reversal - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let final_position = tracker.current_position(); @@ -283,7 +283,7 @@ fn test_zero_cash_reversal() { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 0.0); // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let cash_after_short = tracker.cash_balance(); @@ -295,7 +295,7 @@ fn test_zero_cash_reversal() { // If we had zero cash, attempting Long100 should reject entirely // For now, we'll test with very low cash scenario - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); @@ -319,13 +319,13 @@ fn test_exact_phase1_boundary() { let price = 5_600.0; // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); info!(cash = tracker.cash_balance(), position = tracker.current_position(), "Test 7: Cash after short"); // Attempt reversal - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); info!(cash = tracker.cash_balance(), position = tracker.current_position(), "Test 7: Final cash and position"); @@ -345,12 +345,12 @@ fn test_transaction_cost_verification() { // Test with Market order let mut tracker_market = PortfolioTracker::new(25_000.0, 0.0001, 0.0); - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker_market.execute_action(short_action, price, 1.0); let cash_after_short = tracker_market.cash_balance(); - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker_market.execute_action(long_action, price, 1.0); let cash_after_reversal = tracker_market.cash_balance(); @@ -375,7 +375,7 @@ fn test_negative_cash_guard() { let price = 5_600.0; // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); info!(cash = tracker.cash_balance(), "Test 9: Cash after short"); @@ -383,7 +383,7 @@ fn test_negative_cash_guard() { // If cash were negative, any trade should be rejected // Current implementation has this guard at line ~235 in portfolio_tracker.rs - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); info!(cash = tracker.cash_balance(), position = tracker.current_position(), "Test 9: Final cash and position"); @@ -407,7 +407,7 @@ fn test_maximum_partial_fill() { let mut tracker = PortfolioTracker::new(initial_capital, 0.0001, 0.0); // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let cash_after_short = tracker.cash_balance(); @@ -421,7 +421,7 @@ fn test_maximum_partial_fill() { info!(remaining_cash_for_phase2, affordable_phase2_contracts, "Test 10: Phase 2 capacity"); // Attempt Long100 reversal - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); @@ -442,12 +442,12 @@ fn test_multiple_partial_reversals() { let price = 5_600.0; // Reversal 1: Flat → Short - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); info!(position = tracker.current_position(), cash = tracker.cash_balance(), "Reversal 1: Short"); // Reversal 2: Short → Long - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); info!(position = tracker.current_position(), cash = tracker.cash_balance(), "Reversal 2: Long"); diff --git a/crates/ml/tests/partial_reversal_validation.rs b/crates/ml/tests/partial_reversal_validation.rs index cdcc628c2..87d19feb4 100644 --- a/crates/ml/tests/partial_reversal_validation.rs +++ b/crates/ml/tests/partial_reversal_validation.rs @@ -88,13 +88,13 @@ fn test_full_reversal_sufficient_cash() { let price = 5_600.0; // Step 1: Open short position (-1.0) - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); assert_eq!(tracker.current_position(), -1.0, "Should have -1.0 short position"); // Step 2: Reverse to long (+1.0) with sufficient cash - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); assert_eq!(tracker.current_position(), 1.0, "Should complete full reversal to +1.0 long"); @@ -108,7 +108,7 @@ fn test_partial_reversal_limited_cash() { let price = 5_600.0; // Step 1: Open short position (-1.0) - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let cash_before_reversal = tracker.cash_balance(); @@ -116,7 +116,7 @@ fn test_partial_reversal_limited_cash() { // Step 2: Attempt full reversal to long (+1.0) // With limited cash, should achieve partial reversal - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); @@ -143,7 +143,7 @@ fn test_reversal_to_flat_only() { let price = 5_600.0; // Step 1: Open long position (+1.0) - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let cash_after_long = tracker.cash_balance(); @@ -151,7 +151,7 @@ fn test_reversal_to_flat_only() { // Step 2: Attempt reversal to short (-1.0) // With very limited cash, should only close long (Phase 1), not open short (Phase 2) - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let final_position = tracker.current_position(); @@ -172,13 +172,13 @@ fn test_transaction_costs_tracked() { let price = 5_600.0; // Execute short → long reversal - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let tx_cost_after_short = tracker.transaction_costs(); assert!(tx_cost_after_short > 0.0, "Should have transaction costs from short"); - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let tx_cost_after_reversal = tracker.transaction_costs(); @@ -204,7 +204,7 @@ fn test_negative_cash_guard() { let price = 5_600.0; // Try to open short with minimal capital - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); // Cash should never go negative @@ -218,12 +218,12 @@ fn test_multiple_reversals() { let price = 5_600.0; // Reversal 1: Flat → Short - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); info!(position = tracker.current_position(), cash = tracker.cash_balance(), "After Reversal 1 (Short)"); // Reversal 2: Short → Long - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); info!(position = tracker.current_position(), cash = tracker.cash_balance(), "After Reversal 2 (Long)"); @@ -249,14 +249,14 @@ fn test_cash_reserve_enforcement() { let price = 5_600.0; // Open short position - let short_action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal); + let short_action = FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal); tracker.execute_action(short_action, price, 1.0); let cash_before = tracker.cash_balance(); info!(cash_before, "Cash before reversal"); // Attempt reversal with reserve requirement - let long_action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let long_action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(long_action, price, 1.0); let final_position = tracker.current_position(); diff --git a/crates/ml/tests/pnl_realism_tests.rs b/crates/ml/tests/pnl_realism_tests.rs index 1fe4efa60..d649980f2 100644 --- a/crates/ml/tests/pnl_realism_tests.rs +++ b/crates/ml/tests/pnl_realism_tests.rs @@ -120,9 +120,9 @@ fn test_pnl_in_realistic_range() -> Result<()> { for (i, &price) in price_sequence.iter().enumerate() { let exposure = match i % 3 { - 0 => ExposureLevel::Long100, + 0 => ExposureLevel::LongFull, 1 => ExposureLevel::Flat, - 2 => ExposureLevel::Short50, + 2 => ExposureLevel::ShortFull, _ => ExposureLevel::Flat, }; @@ -171,7 +171,7 @@ fn test_pnl_return_percentage_reasonable() -> Result<()> { for (i, &price) in price_sequence.iter().enumerate() { let exposure = if i % 2 == 0 { - ExposureLevel::Long50 + ExposureLevel::LongSmall } else { ExposureLevel::Flat }; @@ -215,7 +215,7 @@ fn test_pnl_matches_position_changes() -> Result<()> { // Open long position tracker.execute_action( - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), entry_price, position_size, ); @@ -265,7 +265,7 @@ fn test_transaction_costs_reduce_pnl() -> Result<()> { // Open position tracker.execute_action( - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), entry_price, position_size, ); @@ -316,9 +316,9 @@ fn test_pnl_accumulation_correct() -> Result<()> { for step in 0..10 { let price = base_price + (step as f32 * 2.0); let exposure = match step % 3 { - 0 => ExposureLevel::Long50, + 0 => ExposureLevel::LongSmall, 1 => ExposureLevel::Flat, - 2 => ExposureLevel::Short50, + 2 => ExposureLevel::ShortFull, _ => ExposureLevel::Flat, }; @@ -367,7 +367,7 @@ fn test_long_position_pnl_calculation() -> Result<()> { // Open long position at $4500 tracker.execute_action( - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), 4500.0, 100.0, ); @@ -407,7 +407,7 @@ fn test_short_position_pnl_calculation() -> Result<()> { // Open short position at $4500 tracker.execute_action( - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal), 4500.0, 100.0, ); @@ -452,7 +452,7 @@ fn test_pnl_explosion_detector() -> Result<()> { let preprocessed_z_score = -2.5; // This should NEVER be a price tracker.execute_action( - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), preprocessed_z_score, 100.0, ); diff --git a/crates/ml/tests/portfolio_integration_tests.rs b/crates/ml/tests/portfolio_integration_tests.rs index da3f4d2dd..cd8be091d 100644 --- a/crates/ml/tests/portfolio_integration_tests.rs +++ b/crates/ml/tests/portfolio_integration_tests.rs @@ -92,11 +92,11 @@ use tracing::info; // Helper functions for consistent 3-action semantics in tests fn buy_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal) } fn sell_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal) } fn hold_action() -> FactoredAction { diff --git a/crates/ml/tests/portfolio_tracker_reset_test.rs b/crates/ml/tests/portfolio_tracker_reset_test.rs index a903a9a15..67ba5f400 100644 --- a/crates/ml/tests/portfolio_tracker_reset_test.rs +++ b/crates/ml/tests/portfolio_tracker_reset_test.rs @@ -169,7 +169,7 @@ fn test_reset_with_factored_actions() { let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0); // Execute factored action (Long100 = full long exposure) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); tracker.execute_action(action, 100.0, 100.0); // max_position = 100 contracts // Verify position opened diff --git a/crates/ml/tests/portfolio_value_normalization_test.rs b/crates/ml/tests/portfolio_value_normalization_test.rs index 286e59447..71cb75514 100644 --- a/crates/ml/tests/portfolio_value_normalization_test.rs +++ b/crates/ml/tests/portfolio_value_normalization_test.rs @@ -128,12 +128,12 @@ use tracing::info; /// Create a BUY action (Long100 + Market + Normal) fn create_buy_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal) } /// Create a SELL action (Short100 + Market + Normal) fn create_sell_action() -> FactoredAction { - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal) } /// Create a FLAT action (closes position) diff --git a/crates/ml/tests/ppo_45_action_network_tests.rs b/crates/ml/tests/ppo_45_action_network_tests.rs index cb8485ad9..7d9a21ec1 100644 --- a/crates/ml/tests/ppo_45_action_network_tests.rs +++ b/crates/ml/tests/ppo_45_action_network_tests.rs @@ -95,7 +95,7 @@ fn test_policy_network_45_output() -> Result<()> { // Create PPO with 45 actions let config = PPOConfig { - num_actions: 45, // 5x3x3 factored action space + num_actions: 63, // 5x3x3 factored action space state_dim: 54, // Wave 3 features (54->54) ..Default::default() }; @@ -125,7 +125,7 @@ fn test_value_network_single_output() -> Result<()> { // Create PPO with 45 actions let config = PPOConfig { - num_actions: 45, // 5x3x3 factored action space + num_actions: 63, // 5x3x3 factored action space state_dim: 54, // Wave 3 features (54->54) ..Default::default() }; @@ -155,7 +155,7 @@ fn test_policy_network_softmax() -> Result<()> { // Create PPO with 45 actions let config = PPOConfig { - num_actions: 45, + num_actions: 63, state_dim: 54, ..Default::default() }; @@ -208,7 +208,7 @@ fn test_network_forward_pass() -> Result<()> { // Create PPO with 45 actions let config = PPOConfig { - num_actions: 45, + num_actions: 63, state_dim: 54, policy_hidden_dims: vec![128, 64], value_hidden_dims: vec![256, 128, 64], @@ -354,7 +354,7 @@ fn test_hyperopt_adapter_default_45_actions() -> Result<()> { let config = PPOConfig { state_dim: 54, - num_actions: 45, // This is what hyperopt adapter should use + num_actions: 63, // This is what hyperopt adapter should use policy_hidden_dims: vec![128, 64], value_hidden_dims: vec![512, 384, 256, 128, 64], policy_learning_rate: params.policy_learning_rate, diff --git a/crates/ml/tests/ppo_factored_action_tests.rs b/crates/ml/tests/ppo_factored_action_tests.rs index f8c0debb7..8de1755fe 100644 --- a/crates/ml/tests/ppo_factored_action_tests.rs +++ b/crates/ml/tests/ppo_factored_action_tests.rs @@ -86,53 +86,53 @@ fn test_factored_action_index_mapping() { // Test boundary cases let action_0 = FactoredAction::from_index(0).unwrap(); - assert_eq!(action_0.exposure, ExposureLevel::Short100); + assert_eq!(action_0.exposure, ExposureLevel::ShortSmall); assert_eq!(action_0.order, OrderType::Market); assert_eq!(action_0.urgency, Urgency::Patient); assert_eq!(action_0.to_index(), 0); - let action_44 = FactoredAction::from_index(44).unwrap(); - assert_eq!(action_44.exposure, ExposureLevel::Long100); - assert_eq!(action_44.order, OrderType::IoC); - assert_eq!(action_44.urgency, Urgency::Aggressive); - assert_eq!(action_44.to_index(), 44); + let action_62 = FactoredAction::from_index(62).unwrap(); + assert_eq!(action_62.exposure, ExposureLevel::LongFull); + assert_eq!(action_62.order, OrderType::IoC); + assert_eq!(action_62.urgency, Urgency::Aggressive); + assert_eq!(action_62.to_index(), 62); // Test middle case (Flat + Market + Normal) - // index = 2*9 + 0*3 + 1 = 19 - let action_19 = FactoredAction::from_index(19).unwrap(); - assert_eq!(action_19.exposure, ExposureLevel::Flat); - assert_eq!(action_19.order, OrderType::Market); + // index = 3*9 + 0*3 + 1 = 28 + let action_28 = FactoredAction::from_index(28).unwrap(); + assert_eq!(action_28.exposure, ExposureLevel::Flat); + assert_eq!(action_28.order, OrderType::Market); assert_eq!(action_19.urgency, Urgency::Normal); assert_eq!(action_19.to_index(), 19); // Test round-trip for all 45 actions - for idx in 0..45 { + for idx in 0..63 { let action = FactoredAction::from_index(idx).unwrap(); assert_eq!(action.to_index(), idx, "Round-trip failed for index {}", idx); } // Test out of bounds - assert!(FactoredAction::from_index(45).is_err()); + assert!(FactoredAction::from_index(63).is_err()); assert!(FactoredAction::from_index(100).is_err()); } #[test] fn test_factored_action_exposure_levels() { - // Test all 5 exposure levels: -100%, -50%, 0%, +50%, +100% + // Test all 7 exposure levels - let short100 = FactoredAction::new( - ExposureLevel::Short100, + let short_small = FactoredAction::new( + ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal, ); - assert_eq!(short100.target_exposure(), -1.0); + assert_eq!(short_small.target_exposure(), -0.25); - let short50 = FactoredAction::new( - ExposureLevel::Short50, + let short_full = FactoredAction::new( + ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal, ); - assert_eq!(short50.target_exposure(), -0.5); + assert_eq!(short_full.target_exposure(), -1.0); let flat = FactoredAction::new( ExposureLevel::Flat, @@ -141,19 +141,19 @@ fn test_factored_action_exposure_levels() { ); assert_eq!(flat.target_exposure(), 0.0); - let long50 = FactoredAction::new( - ExposureLevel::Long50, + let long_small = FactoredAction::new( + ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal, ); - assert_eq!(long50.target_exposure(), 0.5); + assert_eq!(long_small.target_exposure(), 0.25); - let long100 = FactoredAction::new( - ExposureLevel::Long100, + let long_full = FactoredAction::new( + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); - assert_eq!(long100.target_exposure(), 1.0); + assert_eq!(long_full.target_exposure(), 1.0); } #[test] @@ -215,7 +215,7 @@ fn test_factored_action_to_position_delta() { // Test Long100 from Flat position let long100 = FactoredAction::new( - ExposureLevel::Long100, + ExposureLevel::LongFull, OrderType::Market, Urgency::Normal, ); @@ -236,56 +236,55 @@ fn test_factored_action_to_position_delta() { // delta = 0.0 - 1.0 = -1.0 assert_eq!(delta, -1.0); - // Test Short100 from Flat position - let short100 = FactoredAction::new( - ExposureLevel::Short100, + // Test ShortFull from Flat position + let short_full = FactoredAction::new( + ExposureLevel::ShortFull, OrderType::Market, Urgency::Normal, ); - let delta = short100.to_position_delta(0.0, 2.0); + let delta = short_full.to_position_delta(0.0, 2.0); // target_exposure = -1.0, current = 0.0 - // delta = (-1.0 - 0.0) * 2.0 = -2.0 + // delta = (-1.0 * 2.0) - 0.0 = -2.0 assert_eq!(delta, -2.0); // Test no change (already at target) - let long50 = FactoredAction::new( - ExposureLevel::Long50, + let long_small = FactoredAction::new( + ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal, ); - let delta = long50.to_position_delta(1.0, 2.0); - // target_exposure = 0.5, current = 1.0 - // current_exposure = 1.0 / 2.0 = 0.5 - // delta = (0.5 - 0.5) * 2.0 = 0.0 + let delta = long_small.to_position_delta(0.5, 2.0); + // target_exposure = 0.25, current = 0.5 + // target_position = 0.25 * 2.0 = 0.5 + // delta = 0.5 - 0.5 = 0.0 assert_eq!(delta, 0.0); } #[test] fn test_factored_action_boundary_cases() { - // Test index 0: Short100 + Market + Patient + // Test index 0: ShortSmall + Market + Patient let action_0 = FactoredAction::from_index(0).unwrap(); - assert_eq!(action_0.exposure, ExposureLevel::Short100); + assert_eq!(action_0.exposure, ExposureLevel::ShortSmall); assert_eq!(action_0.order, OrderType::Market); assert_eq!(action_0.urgency, Urgency::Patient); - assert_eq!(action_0.target_exposure(), -1.0); + assert_eq!(action_0.target_exposure(), -0.25); assert_eq!(action_0.transaction_cost(), 0.0015); assert_eq!(action_0.urgency_weight(), 0.5); - // Test index 44: Long100 + IoC + Aggressive - let action_44 = FactoredAction::from_index(44).unwrap(); - assert_eq!(action_44.exposure, ExposureLevel::Long100); - assert_eq!(action_44.order, OrderType::IoC); - assert_eq!(action_44.urgency, Urgency::Aggressive); - assert_eq!(action_44.target_exposure(), 1.0); - assert_eq!(action_44.transaction_cost(), 0.0010); - assert_eq!(action_44.urgency_weight(), 1.5); + // Test index 62: LongFull + IoC + Aggressive + let action_62 = FactoredAction::from_index(62).unwrap(); + assert_eq!(action_62.exposure, ExposureLevel::LongFull); + assert_eq!(action_62.order, OrderType::IoC); + assert_eq!(action_62.urgency, Urgency::Aggressive); + assert_eq!(action_62.target_exposure(), 1.0); + assert_eq!(action_62.transaction_cost(), 0.0010); + assert_eq!(action_62.urgency_weight(), 1.5); - // Test specific known index: 22 = Flat + LimitMaker + Patient - // index = 2*9 + 1*3 + 0 = 18 + 3 + 0 = 21 (wait, let me recalculate) - // index = 2*9 + 1*3 + 0 = 18 + 3 = 21 - let action_21 = FactoredAction::from_index(21).unwrap(); - assert_eq!(action_21.exposure, ExposureLevel::Flat); - assert_eq!(action_21.order, OrderType::LimitMaker); - assert_eq!(action_21.urgency, Urgency::Patient); + // Test specific known index: Flat + LimitMaker + Patient + // index = 3*9 + 1*3 + 0 = 27 + 3 = 30 + let action_30 = FactoredAction::from_index(30).unwrap(); + assert_eq!(action_30.exposure, ExposureLevel::Flat); + assert_eq!(action_30.order, OrderType::LimitMaker); + assert_eq!(action_30.urgency, Urgency::Patient); assert_eq!(action_21.to_index(), 21); } diff --git a/crates/ml/tests/ppo_lstm_training_loop_tests.rs b/crates/ml/tests/ppo_lstm_training_loop_tests.rs index 9ffd940d8..4bc5386c6 100644 --- a/crates/ml/tests/ppo_lstm_training_loop_tests.rs +++ b/crates/ml/tests/ppo_lstm_training_loop_tests.rs @@ -115,7 +115,7 @@ fn test_ppo_training_with_lstm_disabled() { // Test backward compatibility: standard MLP networks should work let config = PPOConfig { state_dim: 32, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![64, 32], value_hidden_dims: vec![64, 32], policy_learning_rate: 3e-4, @@ -170,7 +170,7 @@ fn test_ppo_training_with_lstm_enabled() { // NOTE: LSTM integration now complete via enum-based architecture let config = PPOConfig { state_dim: 32, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![64, 32], value_hidden_dims: vec![64, 32], policy_learning_rate: 3e-4, @@ -227,7 +227,7 @@ fn test_lstm_network_initialization() { // Test that LSTM networks are correctly initialized based on config let lstm_config = PPOConfig { state_dim: 32, - num_actions: 45, + num_actions: 63, use_lstm: true, lstm_hidden_dim: 128, lstm_num_layers: 2, @@ -236,7 +236,7 @@ fn test_lstm_network_initialization() { let mlp_config = PPOConfig { state_dim: 32, - num_actions: 45, + num_actions: 63, use_lstm: false, ..PPOConfig::default() }; diff --git a/crates/ml/tests/ppo_recurrent_integration_tests.rs b/crates/ml/tests/ppo_recurrent_integration_tests.rs index 96fbdfdc4..ecad41e42 100644 --- a/crates/ml/tests/ppo_recurrent_integration_tests.rs +++ b/crates/ml/tests/ppo_recurrent_integration_tests.rs @@ -104,7 +104,7 @@ fn test_recurrent_ppo_single_episode() { // Test that LSTM-enhanced PPO can train on a single episode let config = PPOConfig { state_dim: 32, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![64], value_hidden_dims: vec![64], use_lstm: true, @@ -150,7 +150,7 @@ fn test_recurrent_ppo_single_episode() { let mut trajectory = Trajectory::new(); for t in 0..10 { let state = vec![t as f32; 32]; // Simple incrementing state - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); let log_prob = -1.0; let value = 5.0 + t as f32; let reward = 1.0; @@ -187,7 +187,7 @@ fn test_recurrent_ppo_hidden_state_continuity() { // Test that hidden states persist and evolve across timesteps within an episode let config = PPOConfig { state_dim: 16, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![32], value_hidden_dims: vec![32], use_lstm: true, @@ -232,7 +232,7 @@ fn test_recurrent_ppo_episode_boundaries() { // Test that hidden states reset between episodes let config = PPOConfig { state_dim: 16, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![32], value_hidden_dims: vec![32], use_lstm: true, @@ -253,7 +253,7 @@ fn test_recurrent_ppo_episode_boundaries() { for t in 0..5 { episode1.add_step(TrajectoryStep::new( vec![1.0; 16], - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), -1.0, 5.0, 1.0, @@ -265,7 +265,7 @@ fn test_recurrent_ppo_episode_boundaries() { for t in 0..5 { episode2.add_step(TrajectoryStep::new( vec![2.0; 16], - FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal), -1.0, 5.0, 1.0, @@ -317,7 +317,7 @@ fn test_recurrent_vs_feedforward_ppo() { // Compare LSTM vs non-LSTM PPO training behavior let base_config = PPOConfig { state_dim: 16, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![32], value_hidden_dims: vec![32], batch_size: 16, @@ -352,7 +352,7 @@ fn test_recurrent_vs_feedforward_ppo() { for t in 0..10 { trajectory.add_step(TrajectoryStep::new( vec![t as f32; 16], - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), -1.0, 5.0, 1.0, @@ -396,7 +396,7 @@ fn test_recurrent_ppo_checkpointing() { // to enable loading LSTM checkpoints from safetensors files let config = PPOConfig { state_dim: 16, - num_actions: 45, + num_actions: 63, policy_hidden_dims: vec![32], value_hidden_dims: vec![32], use_lstm: true, @@ -417,7 +417,7 @@ fn test_recurrent_ppo_checkpointing() { for t in 0..10 { trajectory.add_step(TrajectoryStep::new( vec![t as f32; 16], - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), -1.0, 5.0, 1.0, diff --git a/crates/ml/tests/ppo_sequence_batching_multi_episode_tests.rs b/crates/ml/tests/ppo_sequence_batching_multi_episode_tests.rs index 2ad966440..e53ee49d2 100644 --- a/crates/ml/tests/ppo_sequence_batching_multi_episode_tests.rs +++ b/crates/ml/tests/ppo_sequence_batching_multi_episode_tests.rs @@ -179,7 +179,7 @@ fn test_episode_boundary_state_values() { for i in 0..5 { episode1.add_step(TrajectoryStep { state: vec![100.0 + i as f32; 32], // States: 100, 101, 102, 103, 104 - action: FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + action: FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), log_prob: -1.0, value: 0.5, reward: 1.0, @@ -191,7 +191,7 @@ fn test_episode_boundary_state_values() { for i in 0..7 { episode2.add_step(TrajectoryStep { state: vec![200.0 + i as f32; 32], // States: 200, 201, 202, 203, 204, 205, 206 - action: FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), + action: FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal), log_prob: -1.0, value: 0.5, reward: 1.0, @@ -289,7 +289,7 @@ fn test_action_continuity_within_sequences() { for i in 0..5 { episode1.add_step(TrajectoryStep { state: vec![0.0; 32], - action: FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), // All Buy + action: FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), // All Buy log_prob: -1.0, value: 0.5, reward: 1.0, @@ -301,7 +301,7 @@ fn test_action_continuity_within_sequences() { for i in 0..7 { episode2.add_step(TrajectoryStep { state: vec![0.0; 32], - action: FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal), // All Sell + action: FactoredAction::new(ExposureLevel::ShortSmall, OrderType::Market, Urgency::Normal), // All Sell log_prob: -1.0, value: 0.5, reward: 1.0, diff --git a/crates/ml/tests/price_validity_tests.rs b/crates/ml/tests/price_validity_tests.rs index dbd9d41f6..47b5a51e8 100644 --- a/crates/ml/tests/price_validity_tests.rs +++ b/crates/ml/tests/price_validity_tests.rs @@ -113,7 +113,7 @@ fn test_all_prices_positive() -> Result<()> { // Execute action with positive price let action = FactoredAction::new( - ExposureLevel::Long50, + ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal, ); @@ -213,7 +213,7 @@ fn test_no_preprocessed_prices_in_pnl() -> Result<()> { // Execute action let action = if i % 2 == 0 { - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal) + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal) } else { FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal) }; @@ -249,7 +249,7 @@ fn test_training_vs_validation_price_consistency() -> Result<()> { for &price in &training_prices { training_tracker.execute_action( - FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal), price, 100.0, ); @@ -262,7 +262,7 @@ fn test_training_vs_validation_price_consistency() -> Result<()> { for &price in &validation_prices { validation_tracker.execute_action( - FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongSmall, OrderType::Market, Urgency::Normal), price, 100.0, ); @@ -299,11 +299,11 @@ fn test_portfolio_tracker_price_updates() -> Result<()> { // Execute action let exposure = match i % 5 { - 0 => ExposureLevel::Long100, - 1 => ExposureLevel::Long50, + 0 => ExposureLevel::LongFull, + 1 => ExposureLevel::LongSmall, 2 => ExposureLevel::Flat, - 3 => ExposureLevel::Short50, - 4 => ExposureLevel::Short100, + 3 => ExposureLevel::ShortFull, + 4 => ExposureLevel::ShortSmall, _ => ExposureLevel::Flat, }; @@ -346,7 +346,7 @@ fn test_zero_price_protection() -> Result<()> { let mut tracker = PortfolioTracker::new(10_000.0, 0.0001, 1.0); // Attempt to execute action at price = 0 (should be protected) - let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal); + let action = FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal); // Note: PortfolioTracker allows price=0 but normalized_position calculation has fallback tracker.execute_action(action, 0.0, 100.0); @@ -379,7 +379,7 @@ fn test_negative_price_rejection() -> Result<()> { // Execute action (system accepts negative prices, but P&L will be wrong) tracker.execute_action( - FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal), + FactoredAction::new(ExposureLevel::LongFull, OrderType::Market, Urgency::Normal), preprocessed_z_score, 100.0, ); diff --git a/crates/ml/tests/risk_position_limit_integration_test.rs b/crates/ml/tests/risk_position_limit_integration_test.rs index e19a8c633..78220427d 100644 --- a/crates/ml/tests/risk_position_limit_integration_test.rs +++ b/crates/ml/tests/risk_position_limit_integration_test.rs @@ -258,7 +258,7 @@ impl MockPositionLimiter { // Action space: (exposure * 9) + (order_type * 3) + urgency // Exposure: 0=Short100, 1=Short50, 2=Flat, 3=Long50, 4=Long100 - for action_idx in 0..45 { + for action_idx in 0..63 { let exposure_idx = action_idx / 9; let exposure_level = match exposure_idx { 0 => -1.0, // Short100 diff --git a/crates/ml/tests/training_edge_cases.rs b/crates/ml/tests/training_edge_cases.rs index 9a8c867b3..6219f811a 100644 --- a/crates/ml/tests/training_edge_cases.rs +++ b/crates/ml/tests/training_edge_cases.rs @@ -444,7 +444,7 @@ async fn test_ppo_training_with_single_step_trajectory() -> Result<(), Box