diff --git a/Cargo.lock b/Cargo.lock index 06531aa31..b5a53cd08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,7 @@ dependencies = [ "regex", "reqwest 0.12.23", "rust_decimal", + "rustls 0.23.32", "secrecy 0.8.0", "serde", "serde_json", @@ -1652,7 +1653,6 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", - "serde", ] [[package]] @@ -1799,7 +1799,6 @@ dependencies = [ "async-trait", "axum 0.7.9", "base64 0.22.1", - "bigdecimal", "bytes", "chrono", "common", @@ -11309,7 +11308,6 @@ dependencies = [ "axum 0.7.9", "base32", "base64 0.22.1", - "bigdecimal", "bytes", "chrono", "clap", diff --git a/crates/ml/src/data_loader.rs b/crates/ml/src/data_loader.rs index 480f5face..2051dac87 100644 --- a/crates/ml/src/data_loader.rs +++ b/crates/ml/src/data_loader.rs @@ -383,15 +383,15 @@ impl RealDataLoader { Ok(Indicators { rsi: self.calculate_rsi(&closes, 14)?, - macd: self.calculate_macd(&closes, 12, 26)?, - macd_signal: self.calculate_macd_signal(&closes, 12, 26, 9)?, + macd: self.calculate_macd(&closes, 12, 26)?.iter().map(|&x| x as f32).collect(), + macd_signal: self.calculate_macd_signal(&closes, 12, 26, 9)?.iter().map(|&x| x as f32).collect(), bb_upper: self.calculate_bb_upper(&closes, 20, 2.0)?, - bb_middle: self.calculate_sma(&closes, 20)?, + bb_middle: self.calculate_sma(&closes, 20)?.iter().map(|&x| x as f32).collect(), bb_lower: self.calculate_bb_lower(&closes, 20, 2.0)?, atr: self.calculate_atr(bars, 14)?, - ema_fast: self.calculate_ema(&closes, 12)?, - ema_slow: self.calculate_ema(&closes, 26)?, - volume_ma: self.calculate_sma(&volumes, 20)?, + ema_fast: self.calculate_ema(&closes, 12)?.iter().map(|&x| x as f32).collect(), + ema_slow: self.calculate_ema(&closes, 26)?.iter().map(|&x| x as f32).collect(), + volume_ma: self.calculate_sma(&volumes, 20)?.iter().map(|&x| x as f32).collect(), }) } @@ -436,7 +436,10 @@ impl RealDataLoader { } /// Calculate MACD line - fn calculate_macd(&self, prices: &[f64], fast: usize, slow: usize) -> Result> { + /// + /// Returns f64 to avoid precision-losing f64→f32→f64 round-trips + /// when the result feeds back into `calculate_ema` for the signal line. + fn calculate_macd(&self, prices: &[f64], fast: usize, slow: usize) -> Result> { let ema_fast = self.calculate_ema(prices, fast)?; let ema_slow = self.calculate_ema(prices, slow)?; @@ -454,10 +457,9 @@ impl RealDataLoader { fast: usize, slow: usize, signal: usize, - ) -> Result> { + ) -> Result> { let macd = self.calculate_macd(prices, fast, slow)?; - let macd_f64: Vec = macd.iter().map(|&x| x as f64).collect(); - self.calculate_ema(&macd_f64, signal) + self.calculate_ema(&macd, signal) } /// Calculate Bollinger upper band @@ -476,7 +478,7 @@ impl RealDataLoader { let variance = window.iter().map(|&x| (x - mean).powi(2)).sum::() / period as f64; let std_dev = variance.sqrt(); - upper.push((sma[i] as f64 + num_std * std_dev) as f32); + upper.push((sma[i] + num_std * std_dev) as f32); } Ok(upper) @@ -498,7 +500,7 @@ impl RealDataLoader { let variance = window.iter().map(|&x| (x - mean).powi(2)).sum::() / period as f64; let std_dev = variance.sqrt(); - lower.push((sma[i] as f64 - num_std * std_dev) as f32); + lower.push((sma[i] - num_std * std_dev) as f32); } Ok(lower) @@ -540,7 +542,10 @@ impl RealDataLoader { } /// Calculate EMA (Exponential Moving Average) - fn calculate_ema(&self, prices: &[f64], period: usize) -> Result> { + /// + /// Returns f64 to avoid precision-losing f64→f32→f64 round-trips + /// when the result feeds back into further f64 arithmetic (e.g. MACD). + fn calculate_ema(&self, prices: &[f64], period: usize) -> Result> { if prices.len() < period { return Err(anyhow::anyhow!( "Need at least {} prices for EMA (got {})", @@ -554,31 +559,33 @@ impl RealDataLoader { // Start with SMA for first period let initial_sma = prices[..period].iter().sum::() / period as f64; - ema.extend(vec![initial_sma as f32; period]); + ema.extend(vec![initial_sma; period]); // Calculate EMA for remaining values for i in period..prices.len() { - let prev_ema = ema[i - 1] as f64; - let new_ema = alpha * prices[i] + (1.0 - alpha) * prev_ema; - ema.push(new_ema as f32); + let new_ema = alpha * prices[i] + (1.0 - alpha) * ema[i - 1]; + ema.push(new_ema); } Ok(ema) } /// Calculate SMA (Simple Moving Average) - fn calculate_sma(&self, prices: &[f64], period: usize) -> Result> { + /// + /// Returns f64 to avoid precision-losing f64→f32→f64 round-trips + /// when the result feeds back into further f64 arithmetic (e.g. Bollinger bands). + fn calculate_sma(&self, prices: &[f64], period: usize) -> Result> { let mut sma = Vec::with_capacity(prices.len()); for i in 0..prices.len() { if i < period - 1 { - sma.push(prices[i] as f32); + sma.push(prices[i]); continue; } let window = &prices[i - period + 1..=i]; let avg = window.iter().sum::() / period as f64; - sma.push(avg as f32); + sma.push(avg); } Ok(sma) diff --git a/crates/ml/src/dqn/rainbow_agent_impl.rs b/crates/ml/src/dqn/rainbow_agent_impl.rs index e44c0749d..cda68170e 100644 --- a/crates/ml/src/dqn/rainbow_agent_impl.rs +++ b/crates/ml/src/dqn/rainbow_agent_impl.rs @@ -14,7 +14,7 @@ use crate::dqn::mixed_precision::training_dtype; use candle_optimisers::adam::ParamsAdam; use tracing::{debug, info}; -use super::multi_step::{create_multi_step_transition, MultiStepCalculator, MultiStepTransition}; +use super::multi_step::{MultiStepCalculator, MultiStepTransition}; use super::rainbow_config::{RainbowAgentConfig, RainbowAgentMetrics, TrainingResult}; use super::rainbow_network::RainbowNetwork; use super::{Experience, ReplayBuffer, ReplayBufferConfig}; @@ -169,16 +169,6 @@ impl RainbowAgent { /// Add experience to replay buffer and multi-step calculator pub fn add_experience(&self, experience: Experience) -> Result<(), MLError> { - // Convert experience to multi-step transition - let _transition = create_multi_step_transition( - experience.state.iter().map(|&x| x as f64).collect(), - experience.action as i64, - experience.reward as f64, - experience.next_state.iter().map(|&x| x as f64).collect(), - experience.done, - 0, // timestep will be set by calculator - ); - // Add to replay buffer { let buffer = self.replay_buffer.lock().map_err(|_| { diff --git a/crates/ml/src/hyperopt/adapters/ppo.rs b/crates/ml/src/hyperopt/adapters/ppo.rs index d38026070..7f03842d7 100644 --- a/crates/ml/src/hyperopt/adapters/ppo.rs +++ b/crates/ml/src/hyperopt/adapters/ppo.rs @@ -1695,7 +1695,7 @@ impl PPOTrainer { // Apply composite risk-adjusted reward (downside dev + differential return) if let Some(ref mut comp) = composite_reward { - reward = comp.compute(reward as f64) as f32; + reward = comp.compute(reward); } // Apply reward shaping (hold penalty + rolling Sharpe + diversity bonus) @@ -1713,11 +1713,11 @@ impl PPOTrainer { false }; reward = shaper.shape_reward( - reward as f64, + reward, is_flat, has_signal, action.to_index(), - ) as f32; + ); } let done = step_idx + 1 >= max_episode_length; diff --git a/crates/ml/src/ppo/composite_reward.rs b/crates/ml/src/ppo/composite_reward.rs index 99e1711d0..f875be824 100644 --- a/crates/ml/src/ppo/composite_reward.rs +++ b/crates/ml/src/ppo/composite_reward.rs @@ -75,7 +75,8 @@ impl CompositeReward { /// /// # Returns /// Composite reward combining all components - pub fn compute(&mut self, raw_return: f64) -> f64 { + pub fn compute(&mut self, raw_return: f32) -> f32 { + let raw_return = f64::from(raw_return); // Update portfolio tracking self.current_value *= 1.0 + raw_return; if self.current_value > self.peak_value { @@ -103,7 +104,7 @@ impl CompositeReward { let differential = self.compute_differential_return(raw_return); let differential_component = differential * self.config.differential_weight; - return_component + downside_component + differential_component + (return_component + downside_component + differential_component) as f32 } /// Compute downside deviation (semi-variance of negative returns) @@ -184,12 +185,12 @@ mod tests { #[test] fn test_composite_reward_positive_return() { let mut reward = CompositeReward::new(); - let r = reward.compute(0.01); // 1% return + let r = reward.compute(0.01_f32); // 1% return // With only 1 data point, downside=0, differential=0 (baseline < 5) // So reward = 0.01 * 1.0 = 0.01 assert!(r > 0.0, "Positive return should yield positive reward, got {r}"); assert!( - (r - 0.01).abs() < 1e-10, + (r - 0.01_f32).abs() < 1e-6, "Single positive return should equal return_weight * raw_return" ); } @@ -197,7 +198,7 @@ mod tests { #[test] fn test_composite_reward_negative_return() { let mut reward = CompositeReward::new(); - let r = reward.compute(-0.02); // -2% return + let r = reward.compute(-0.02_f32); // -2% return // With only 1 data point, downside=0 (need >=2 in window), differential=0 // So reward = -0.02 * 1.0 = -0.02 assert!(r < 0.0, "Negative return should yield negative reward, got {r}"); @@ -207,10 +208,10 @@ mod tests { fn test_composite_reward_downside_deviation() { let mut reward = CompositeReward::new(); // Feed a sequence with several losses to build up downside deviation - let returns = [ + let returns: [f32; 10] = [ -0.01, -0.02, -0.015, -0.005, 0.003, -0.01, -0.02, 0.001, -0.008, -0.012, ]; - let mut last_reward = 0.0; + let mut last_reward = 0.0_f32; for &ret in &returns { last_reward = reward.compute(ret); } @@ -228,10 +229,10 @@ mod tests { let mut reward = CompositeReward::new(); // Build a baseline of small returns for _ in 0..10 { - reward.compute(0.001); // 0.1% baseline + reward.compute(0.001_f32); // 0.1% baseline } // Now a large positive return should get differential bonus - let big_return = 0.05; // 5% return — well above 0.1% baseline + let big_return = 0.05_f32; // 5% return — well above 0.1% baseline let r = reward.compute(big_return); // The differential component should be positive (outperforming baseline) let pure_return = big_return * 1.0; @@ -247,25 +248,25 @@ mod tests { fn test_composite_reward_drawdown_tracking() { let mut reward = CompositeReward::new(); // Go up then down - reward.compute(0.10); // Portfolio: 1.0 * 1.10 = 1.10 + reward.compute(0.10_f32); // Portfolio: 1.0 * 1.10 = 1.10 assert!( - (reward.current_drawdown()).abs() < 1e-10, + (reward.current_drawdown()).abs() < 1e-6, "No drawdown at peak" ); - reward.compute(-0.05); // Portfolio: 1.10 * 0.95 = 1.045 + reward.compute(-0.05_f32); // Portfolio: 1.10 * 0.95 = 1.045 let dd = reward.current_drawdown(); assert!(dd > 0.0, "Should have drawdown after loss from peak"); - // Expected: 1 - 1.045/1.10 = 0.05 + // Expected: 1 - 1.045/1.10 = 0.05 (tolerance accounts for f32 input promotion) assert!( - (dd - 0.05).abs() < 1e-10, + (dd - 0.05).abs() < 1e-6, "Drawdown should be ~5%, got {dd}" ); // Go to new peak - reward.compute(0.10); // Portfolio: 1.045 * 1.10 = 1.1495 + reward.compute(0.10_f32); // Portfolio: 1.045 * 1.10 = 1.1495 assert!( - reward.current_drawdown().abs() < 1e-10, + reward.current_drawdown().abs() < 1e-6, "Should be at new peak, no drawdown" ); } @@ -275,7 +276,7 @@ mod tests { let mut reward = CompositeReward::new(); // Accumulate some state for i in 0..30 { - reward.compute(if i % 2 == 0 { 0.01 } else { -0.005 }); + reward.compute(if i % 2 == 0 { 0.01_f32 } else { -0.005_f32 }); } assert!(!reward.return_window.is_empty()); assert!(!reward.baseline_window.is_empty()); @@ -297,10 +298,10 @@ mod tests { baseline_period: 30, }; let mut reward = CompositeReward::with_config(config); - let r = reward.compute(0.01); + let r = reward.compute(0.01_f32); // With custom return_weight=2.0, single step: 0.01 * 2.0 = 0.02 assert!( - (r - 0.02).abs() < 1e-10, + (r - 0.02_f32).abs() < 1e-6, "Custom return_weight should scale return: got {r}" ); } @@ -309,9 +310,9 @@ mod tests { fn test_composite_reward_all_positive() { let mut reward = CompositeReward::new(); // All positive returns — no downside deviation penalty - let mut last = 0.0; + let mut last = 0.0_f32; for _ in 0..25 { - last = reward.compute(0.005); + last = reward.compute(0.005_f32); } // Downside deviation should be 0 since no negative returns // The differential component may be close to 0 (all returns are similar) @@ -323,7 +324,7 @@ mod tests { // With 25 data points, baseline mean ~ 0.005, so differential ~ 0 // Reward ~ 0.005 * 1.0 + 0 + 0 = 0.005 assert!( - (last - 0.005).abs() < 0.001, + (last - 0.005_f32).abs() < 0.001, "All-positive sequence should have reward near raw return, got {last}" ); } @@ -332,7 +333,7 @@ mod tests { fn test_composite_reward_mixed_sequence() { let mut reward = CompositeReward::new(); // Realistic trading sequence: mostly small gains, occasional larger losses - let returns = [ + let returns: [f32; 20] = [ 0.002, 0.001, -0.003, 0.004, 0.001, -0.002, 0.003, -0.005, 0.002, 0.001, -0.001, 0.003, 0.002, -0.004, 0.001, 0.002, -0.001, 0.003, -0.002, 0.001, ]; diff --git a/crates/ml/src/ppo/percentile_scaler.rs b/crates/ml/src/ppo/percentile_scaler.rs index b59bd2876..37d04985e 100644 --- a/crates/ml/src/ppo/percentile_scaler.rs +++ b/crates/ml/src/ppo/percentile_scaler.rs @@ -74,17 +74,20 @@ impl PercentileScaler { /// Update running percentiles from a batch of values. /// + /// Accepts `&[f32]` at the API boundary; converts to f64 internally for + /// sorting and percentile computation to preserve numerical precision. + /// /// Sorts the batch, computes exact P5/P95, then applies EMA update: /// `p_new = decay * p_old + (1 - decay) * p_actual` /// /// On first call, directly sets `p5`/`p95` to the actual percentiles /// (no EMA blending with zero). - pub fn update(&mut self, values: &[f64]) { + pub fn update(&mut self, values: &[f32]) { if values.is_empty() { return; } - let mut sorted = values.to_vec(); + let mut sorted: Vec = values.iter().map(|&v| f64::from(v)).collect(); sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); let n = sorted.len(); @@ -124,25 +127,28 @@ impl PercentileScaler { /// Scale a value by the running percentile range. /// + /// Accepts f32, computes in f64 internally, returns f32. /// Returns `value / max(P95 - P5, min_scale)`. /// If not yet initialized, returns the value unchanged. #[inline] - pub fn scale(&self, value: f64) -> f64 { + pub fn scale(&self, value: f32) -> f32 { if !self.initialized { return value; } let range = (self.p95 - self.p5).max(self.min_scale); - value / range + (f64::from(value) / range) as f32 } - /// Scale a batch of values in-place. - pub fn scale_batch(&self, values: &mut [f64]) { + /// Scale a batch of f32 values in-place. + /// + /// Computes the range in f64, then divides each element. + pub fn scale_batch(&self, values: &mut [f32]) { if !self.initialized { return; } let range = (self.p95 - self.p5).max(self.min_scale); for v in values.iter_mut() { - *v /= range; + *v = (f64::from(*v) / range) as f32; } } @@ -177,22 +183,23 @@ impl PercentileScaler { mod tests { use super::*; - const EPSILON: f64 = 1e-6; + const EPSILON: f32 = 1e-6; + const EPSILON_F64: f64 = 1e-6; #[test] fn test_percentile_scaler_new() { let scaler = PercentileScaler::new(); assert!(!scaler.is_initialized()); assert!(scaler.range().is_none()); - assert!((scaler.p5() - 0.0).abs() < EPSILON); - assert!((scaler.p95() - 0.0).abs() < EPSILON); + assert!((scaler.p5() - 0.0).abs() < EPSILON_F64); + assert!((scaler.p95() - 0.0).abs() < EPSILON_F64); } #[test] fn test_percentile_scaler_first_update() { let mut scaler = PercentileScaler::new(); // 100 values from 0..100 - let values: Vec = (0..100).map(|i| i as f64).collect(); + let values: Vec = (0..100).map(|i| i as f32).collect(); scaler.update(&values); assert!(scaler.is_initialized()); @@ -222,7 +229,7 @@ mod tests { let u = (i as f64 + 0.5) / n as f64; // Approximate inverse normal CDF (Beasley-Springer-Moro) let z = approximate_inv_normal(u); - values.push(z); + values.push(z as f32); } scaler.update(&values); @@ -243,11 +250,11 @@ mod tests { fn test_percentile_scaler_heavy_tail() { // Heavy-tailed data: most values small, a few large outliers let mut scaler = PercentileScaler::new(); - let mut values: Vec = (0..990).map(|i| (i as f64 - 495.0) / 495.0).collect(); + let mut values: Vec = (0..990).map(|i| ((i as f32 - 495.0) / 495.0)).collect(); // Add extreme outliers (1% each tail) for _ in 0..5 { - values.push(-100.0); - values.push(100.0); + values.push(-100.0_f32); + values.push(100.0_f32); } scaler.update(&values); @@ -264,7 +271,7 @@ mod tests { fn test_percentile_scaler_min_scale() { // Constant values should not cause divide-by-zero let mut scaler = PercentileScaler::new(); - let values = vec![5.0; 100]; + let values = vec![5.0_f32; 100]; scaler.update(&values); // P5 == P95, so range falls back to min_scale @@ -275,9 +282,9 @@ mod tests { ); // Scaling should not produce Inf or NaN - let scaled = scaler.scale(10.0); + let scaled = scaler.scale(10.0_f32); assert!(scaled.is_finite(), "scaled value should be finite"); - assert!((scaled - 10.0).abs() < EPSILON, "10.0 / 1.0 = 10.0, got {scaled}"); + assert!((scaled - 10.0_f32).abs() < EPSILON, "10.0 / 1.0 = 10.0, got {scaled}"); } #[test] @@ -286,13 +293,13 @@ mod tests { let mut scaler = PercentileScaler::with_params(0.9, 1.0); // First batch: values in [0, 10] - let batch1: Vec = (0..100).map(|i| i as f64 / 10.0).collect(); + let batch1: Vec = (0..100).map(|i| i as f32 / 10.0).collect(); scaler.update(&batch1); let p5_after_first = scaler.p5(); let p95_after_first = scaler.p95(); // Second batch: values in [100, 200] - let batch2: Vec = (0..100).map(|i| 100.0 + i as f64).collect(); + let batch2: Vec = (0..100).map(|i| 100.0_f32 + i as f32).collect(); scaler.update(&batch2); // P5 should have moved toward batch2's P5 (≈ 104.95), but EMA dampens @@ -321,21 +328,21 @@ mod tests { // Realistic trading return magnitudes: // Most returns are tiny (-0.001 to +0.001), occasional larger moves let mut scaler = PercentileScaler::new(); - let mut values = Vec::new(); + let mut values: Vec = Vec::new(); // 90% tiny returns in [-0.002, 0.002] for i in 0..900 { - let r = (i as f64 - 450.0) / 450.0 * 0.002; + let r = (i as f32 - 450.0) / 450.0 * 0.002; values.push(r); } // 8% moderate returns in [-0.02, 0.02] for i in 0..80 { - let r = (i as f64 - 40.0) / 40.0 * 0.02; + let r = (i as f32 - 40.0) / 40.0 * 0.02; values.push(r); } // 2% large returns in [-0.1, 0.1] for i in 0..20 { - let r = (i as f64 - 10.0) / 10.0 * 0.1; + let r = (i as f32 - 10.0) / 10.0 * 0.1; values.push(r); } @@ -361,7 +368,7 @@ mod tests { ); // Scaling a normal return should produce a reasonable magnitude - let typical = scaler.scale(0.001); + let typical = scaler.scale(0.001_f32); assert!( typical.is_finite(), "scaled typical return should be finite" @@ -371,23 +378,23 @@ mod tests { #[test] fn test_percentile_scaler_scale_batch() { let mut scaler = PercentileScaler::new(); - let values: Vec = (0..100).map(|i| i as f64).collect(); + let values: Vec = (0..100).map(|i| i as f32).collect(); scaler.update(&values); - let mut batch = vec![10.0, 20.0, 50.0]; + let mut batch = vec![10.0_f32, 20.0, 50.0]; let range = scaler.range().unwrap_or(1.0); scaler.scale_batch(&mut batch); assert!( - (batch[0] - 10.0 / range).abs() < EPSILON, + (f64::from(batch[0]) - 10.0 / range).abs() < EPSILON_F64, "batch[0] should be 10/range" ); assert!( - (batch[1] - 20.0 / range).abs() < EPSILON, + (f64::from(batch[1]) - 20.0 / range).abs() < EPSILON_F64, "batch[1] should be 20/range" ); assert!( - (batch[2] - 50.0 / range).abs() < EPSILON, + (f64::from(batch[2]) - 50.0 / range).abs() < EPSILON_F64, "batch[2] should be 50/range" ); } @@ -402,24 +409,24 @@ mod tests { #[test] fn test_percentile_scaler_single_value() { let mut scaler = PercentileScaler::new(); - scaler.update(&[42.0]); + scaler.update(&[42.0_f32]); assert!(scaler.is_initialized()); // P5 == P95 == 42.0, range falls back to min_scale - assert!((scaler.p5() - 42.0).abs() < EPSILON); - assert!((scaler.p95() - 42.0).abs() < EPSILON); + assert!((scaler.p5() - 42.0).abs() < EPSILON_F64); + assert!((scaler.p95() - 42.0).abs() < EPSILON_F64); } #[test] fn test_percentile_scaler_uninitialized_passthrough() { let scaler = PercentileScaler::new(); // Before initialization, scale should be passthrough - assert!((scaler.scale(42.0) - 42.0).abs() < EPSILON); + assert!((scaler.scale(42.0_f32) - 42.0_f32).abs() < EPSILON); - let mut batch = vec![1.0, 2.0, 3.0]; + let mut batch = vec![1.0_f32, 2.0, 3.0]; scaler.scale_batch(&mut batch); - assert!((batch[0] - 1.0).abs() < EPSILON); - assert!((batch[1] - 2.0).abs() < EPSILON); - assert!((batch[2] - 3.0).abs() < EPSILON); + assert!((batch[0] - 1.0_f32).abs() < EPSILON); + assert!((batch[1] - 2.0_f32).abs() < EPSILON); + assert!((batch[2] - 3.0_f32).abs() < EPSILON); } // ---- Helper for approximate normal distribution ---- diff --git a/crates/ml/src/ppo/ppo.rs b/crates/ml/src/ppo/ppo.rs index 817d13e7c..dd612f021 100644 --- a/crates/ml/src/ppo/ppo.rs +++ b/crates/ml/src/ppo/ppo.rs @@ -31,7 +31,7 @@ use crate::dqn::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig}; use crate::dqn::mixed_precision::training_dtype; use crate::dqn::portfolio_tracker::PortfolioTracker; use crate::dqn::xavier_init::linear_xavier; -use crate::dqn::reward::RewardNormalizer; +use crate::ppo::reward_normalizer::RewardNormalizer; use crate::common::action::FactoredAction; use crate::MLError; @@ -965,24 +965,20 @@ impl PPO { // Apply reward normalization if enabled if let Some(ref mut normalizer) = self.reward_normalizer { for reward in &batch.rewards { - normalizer.update(*reward as f64); + normalizer.update(*reward); } // Normalize all rewards in batch let normalized_rewards: Vec = batch.rewards.iter() - .map(|&r| { - let norm = normalizer.normalize(r as f64); - norm.clamp(-1.0, 1.0) as f32 - }) + .map(|&r| normalizer.normalize(r).clamp(-1.0_f32, 1.0_f32)) .collect(); batch.rewards = normalized_rewards; } // Normalize advantages (percentile scaling or standard mean/std) if let Some(ref mut scaler) = self.percentile_scaler { - let adv_f64: Vec = batch.advantages.iter().map(|&a| a as f64).collect(); - scaler.update(&adv_f64); + scaler.update(&batch.advantages); for adv in &mut batch.advantages { - *adv = scaler.scale(*adv as f64) as f32; + *adv = scaler.scale(*adv); } } else { batch.normalize_advantages()?; @@ -1028,23 +1024,19 @@ impl PPO { // Apply reward normalization if enabled if let Some(ref mut normalizer) = self.reward_normalizer { for reward in &batch.rewards { - normalizer.update(*reward as f64); + normalizer.update(*reward); } let normalized_rewards: Vec = batch.rewards.iter() - .map(|&r| { - let norm = normalizer.normalize(r as f64); - norm.clamp(-1.0, 1.0) as f32 - }) + .map(|&r| normalizer.normalize(r).clamp(-1.0_f32, 1.0_f32)) .collect(); batch.rewards = normalized_rewards; } // Normalize advantages (needed for returns computation consistency) if let Some(ref mut scaler) = self.percentile_scaler { - let adv_f64: Vec = batch.advantages.iter().map(|&a| a as f64).collect(); - scaler.update(&adv_f64); + scaler.update(&batch.advantages); for adv in &mut batch.advantages { - *adv = scaler.scale(*adv as f64) as f32; + *adv = scaler.scale(*adv); } } else { batch.normalize_advantages()?; diff --git a/crates/ml/src/ppo/reward_normalizer.rs b/crates/ml/src/ppo/reward_normalizer.rs index 05429a6f0..7d35b777e 100644 --- a/crates/ml/src/ppo/reward_normalizer.rs +++ b/crates/ml/src/ppo/reward_normalizer.rs @@ -92,7 +92,8 @@ impl RewardNormalizer { /// - After 500 steps: weight = 0.007 (99.3% decay) /// /// This allows the mean to "forget" old regimes and adapt to new ones. - pub fn update(&mut self, value: f64) { + pub fn update(&mut self, value: f32) { + let value = f64::from(value); if !self.initialized { // First sample: initialize mean and variance self.mean = value; @@ -101,7 +102,7 @@ impl RewardNormalizer { } else { // EMA update for mean self.mean = self.alpha * value + (1.0 - self.alpha) * self.mean; - + // EMA update for variance let diff = value - self.mean; self.variance = self.beta * diff.powi(2) + (1.0 - self.beta) * self.variance; @@ -112,20 +113,21 @@ impl RewardNormalizer { /// /// # Returns /// `(value - mean) / std` if initialized and std > epsilon, else value unchanged - pub fn normalize(&self, value: f64) -> f64 { + pub fn normalize(&self, value: f32) -> f32 { // Need at least one sample to have valid statistics if !self.initialized { return value; } + let value = f64::from(value); let std = self.variance.sqrt(); // Avoid division by zero for constant values if std < self.epsilon { - return value; + return value as f32; } - (value - self.mean) / std + ((value - self.mean) / std) as f32 } /// Get current mean and standard deviation diff --git a/crates/ml/src/ppo/reward_shaping.rs b/crates/ml/src/ppo/reward_shaping.rs index 3ca179ce8..952c23c09 100644 --- a/crates/ml/src/ppo/reward_shaping.rs +++ b/crates/ml/src/ppo/reward_shaping.rs @@ -84,11 +84,12 @@ impl PPORewardShaper { /// `shaped = raw_reward + hold_penalty + sharpe_component + diversity_bonus` pub fn shape_reward( &mut self, - raw_reward: f64, + raw_reward: f32, is_flat: bool, has_signal: bool, action_index: usize, - ) -> f64 { + ) -> f32 { + let raw_reward = f64::from(raw_reward); let mut shaped = raw_reward; // 1. Hold penalty: penalize flat position when signals exist @@ -123,7 +124,7 @@ impl PPORewardShaper { shaped += self.diversity_weight * normalized * normalized; } - shaped + shaped as f32 } /// Compute rolling Sharpe ratio from return window. @@ -204,8 +205,8 @@ mod tests { fn test_reward_shaping_no_penalty_when_not_flat() { let mut shaper = PPORewardShaper::new(0.5, 0.0, 0.0); // Not flat => no hold penalty even with signal - let shaped = shaper.shape_reward(1.0, false, true, 0); - assert!((shaped - 1.0).abs() < 1e-10, "Expected 1.0, got {}", shaped); + let shaped = shaper.shape_reward(1.0_f32, false, true, 0); + assert!((shaped - 1.0_f32).abs() < 1e-6, "Expected 1.0, got {}", shaped); } #[test] @@ -213,14 +214,14 @@ mod tests { let mut shaper = PPORewardShaper::new(0.5, 0.0, 0.0); // Flat with signal => penalty applied - let shaped = shaper.shape_reward(1.0, true, true, 0); - assert!((shaped - 0.5).abs() < 1e-10, "Expected 0.5, got {}", shaped); + let shaped = shaper.shape_reward(1.0_f32, true, true, 0); + assert!((shaped - 0.5_f32).abs() < 1e-6, "Expected 0.5, got {}", shaped); // Flat without signal => no penalty shaper.reset(); - let shaped_no_signal = shaper.shape_reward(1.0, true, false, 0); + let shaped_no_signal = shaper.shape_reward(1.0_f32, true, false, 0); assert!( - (shaped_no_signal - 1.0).abs() < 1e-10, + (shaped_no_signal - 1.0_f32).abs() < 1e-6, "Expected 1.0, got {}", shaped_no_signal ); @@ -232,13 +233,13 @@ mod tests { // Feed consistent positive returns to build up Sharpe for _ in 0..10 { - shaper.shape_reward(1.0, false, false, 0); + shaper.shape_reward(1.0_f32, false, false, 0); } // All returns identical => std ~ 0, Sharpe returns 0 (division guard) // Feed slightly varied positive returns instead shaper.reset(); for i in 0..10 { - let reward = 1.0 + (i as f64) * 0.1; + let reward = 1.0_f32 + (i as f32) * 0.1; shaper.shape_reward(reward, false, false, 0); } @@ -247,7 +248,7 @@ mod tests { assert!(sharpe > 0.0, "Expected positive Sharpe, got {}", sharpe); // Shaped reward should be above raw because sharpe_weight * sharpe > 0 - let raw = 1.5; + let raw = 1.5_f32; let shaped = shaper.shape_reward(raw, false, false, 0); assert!(shaped > raw, "Expected shaped ({}) > raw ({})", shaped, raw); } @@ -257,19 +258,19 @@ mod tests { // Diverse actions let mut diverse_shaper = PPORewardShaper::new(0.0, 0.0, 1.0); for i in 0..20 { - diverse_shaper.shape_reward(0.0, false, false, i % NUM_FACTORED_ACTIONS); + diverse_shaper.shape_reward(0.0_f32, false, false, i % NUM_FACTORED_ACTIONS); } // Repeated single action let mut mono_shaper = PPORewardShaper::new(0.0, 0.0, 1.0); for _ in 0..20 { - mono_shaper.shape_reward(0.0, false, false, 0); + mono_shaper.shape_reward(0.0_f32, false, false, 0); } // The diverse shaper should yield higher cumulative diversity bonus. // We check the next call: diverse should give more bonus. - let diverse_reward = diverse_shaper.shape_reward(0.0, false, false, 5); - let mono_reward = mono_shaper.shape_reward(0.0, false, false, 0); + let diverse_reward = diverse_shaper.shape_reward(0.0_f32, false, false, 5); + let mono_reward = mono_shaper.shape_reward(0.0_f32, false, false, 0); assert!( diverse_reward > mono_reward, "Diverse ({}) should exceed monotone ({})", @@ -284,7 +285,7 @@ mod tests { // Accumulate some state for i in 0..15 { - shaper.shape_reward(1.0 + i as f64 * 0.1, false, false, i % NUM_FACTORED_ACTIONS); + shaper.shape_reward(1.0_f32 + i as f32 * 0.1, false, false, i % NUM_FACTORED_ACTIONS); } assert!(!shaper.return_window.is_empty()); assert!(shaper.total_actions > 0); @@ -310,12 +311,12 @@ mod tests { // Build up enough state for all components to fire for i in 0..15 { - let r = 0.5 + (i as f64) * 0.05; + let r = 0.5_f32 + (i as f32) * 0.05; shaper.shape_reward(r, false, false, i % NUM_FACTORED_ACTIONS); } // Now shape a reward where all three components activate - let raw = 1.0; + let raw = 1.0_f32; let shaped = shaper.shape_reward(raw, true, true, 3); // Hold penalty subtracts, Sharpe adds (positive returns), diversity adds @@ -337,9 +338,9 @@ mod tests { // With fewer than MIN_SHARPE_WINDOW returns, no Sharpe component for _ in 0..4 { - let shaped = shaper.shape_reward(1.0, false, false, 0); + let shaped = shaper.shape_reward(1.0_f32, false, false, 0); assert!( - (shaped - 1.0).abs() < 1e-10, + (shaped - 1.0_f32).abs() < 1e-6, "Expected no Sharpe with <5 returns, got {}", shaped ); @@ -347,7 +348,7 @@ mod tests { // 5th return triggers Sharpe computation // But all returns are identical (1.0) => std ~ 0 => Sharpe returns 0 - let shaped_5th = shaper.shape_reward(2.0, false, false, 0); + let shaped_5th = shaper.shape_reward(2.0_f32, false, false, 0); // Returns are [1.0, 1.0, 1.0, 1.0, 2.0] — mean=1.2, std>0, Sharpe>0 assert!( shaped_5th > 2.0, diff --git a/services/broker_gateway_service/Cargo.toml b/services/broker_gateway_service/Cargo.toml index dbd4b80e0..686020439 100644 --- a/services/broker_gateway_service/Cargo.toml +++ b/services/broker_gateway_service/Cargo.toml @@ -57,7 +57,6 @@ thiserror.workspace = true uuid.workspace = true num-traits.workspace = true rust_decimal.workspace = true -bigdecimal.workspace = true # Internal workspace crates common = { workspace = true, features = ["database"] } diff --git a/services/trading_service/Cargo.toml b/services/trading_service/Cargo.toml index aa4ba9328..2aedecf6e 100644 --- a/services/trading_service/Cargo.toml +++ b/services/trading_service/Cargo.toml @@ -77,7 +77,6 @@ md5.workspace = true hostname = "0.4" num-traits.workspace = true rust_decimal.workspace = true -bigdecimal.workspace = true rand.workspace = true sysinfo = "0.33" log.workspace = true