# Numeric Type Standardization Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace all raw f64 financial values with domain-typed i64 fixed-point wrappers (Price, Quantity, Money, Ratio) to eliminate precision-lossy casting and enforce type safety across 37+ crates. **Architecture:** Four newtype wrappers in `crates/common/src/financial_types.rs` — Price/Quantity/Money use i64 with 6dp scale (1_000_000), Ratio wraps f64 with NaN/Inf rejection. All cross-type arithmetic is explicit. The existing u64-based Price/Quantity in `common/src/types.rs` and i64-based IntegerPrice/IntegerQuantity/IntegerMoney in `trading_engine/src/types/financial.rs` are consolidated and deleted. **Tech Stack:** Rust, rust_decimal (Decimal interop), serde (serialization), saturating arithmetic **Scale:** ~11,000 f64 occurrences, ~23,730 `as` casts, ~670 test functions touching price/quantity across 161 files. Phased bottom-up migration with worktree isolation. --- ## Existing Types Being Replaced | Type | Location | Internal | Scale | Adoption | Fate | |------|----------|----------|-------|----------|------| | `Price` | `common/src/types.rs:2256` | `u64` | 100_000_000 (8dp) | ~30 files | **Replace** (can't go negative) | | `Quantity` | `common/src/types.rs:2679` | `u64` | 100_000_000 (8dp) | ~30 files | **Replace** (can't go negative) | | `Quantity` | `common/src/trading.rs:102` | `u64` | 1_000_000 (6dp) | ~5 files | **Delete** (duplicate) | | `Money` | `common/src/types.rs:3793` | `Decimal + Currency` | arbitrary | 1 file | **Replace** (wrong shape) | | `IntegerPrice` | `trading_engine/src/types/financial.rs:42` | `i64` | 1_000_000 (6dp) | 3 files | **Absorb** (becomes canonical Price) | | `IntegerQuantity` | `trading_engine/src/types/financial.rs:227` | `i64` | 1_000_000 (6dp) | 3 files | **Absorb** (becomes canonical Quantity) | | `IntegerMoney` | `trading_engine/src/types/financial.rs:372` | `i64` | 1_000_000 (6dp) | 3 files | **Absorb** (becomes canonical Money) | ## Key Files With Tests - `crates/common/tests/helper_functions_comprehensive_tests.rs` — 49 price/qty test functions - `crates/common/tests/types_comprehensive_tests.rs` — 34 price/qty test functions - `crates/trading_engine/src/types/financial.rs` — 41 inline test functions - `crates/trading_engine/src/types/tests/financial_tests.rs` — 15 test functions - `crates/trading_engine/src/types/tests/basic_focused_tests.rs` — 19 test functions - `crates/trading_engine/src/types/financial_safe.rs` — 36 test functions --- ## Phase 1: Foundation Types in common/ **Worktree:** single worktree, sequential tasks **Goal:** Create the canonical financial types module, wire it into common's public API ### Task 1.1: Create financial_types module with Price **Files:** - Create: `crates/common/src/financial_types.rs` - Modify: `crates/common/src/lib.rs` **Step 1: Write the failing test** Create the test inline at the bottom of the new module: ```rust // At bottom of crates/common/src/financial_types.rs #[cfg(test)] mod tests { use super::*; #[test] fn price_from_f64_roundtrip() { let p = Price::from_f64(123.456789).unwrap(); let diff = (p.to_f64() - 123.456789).abs(); assert!(diff < 1e-6, "Roundtrip failed: diff={diff}"); } #[test] fn price_from_f64_rejects_negative() { assert!(Price::from_f64(-1.0).is_err()); } #[test] fn price_from_f64_rejects_nan() { assert!(Price::from_f64(f64::NAN).is_err()); } #[test] fn price_from_f64_rejects_inf() { assert!(Price::from_f64(f64::INFINITY).is_err()); } #[test] fn price_zero_constant() { assert_eq!(Price::ZERO.to_f64(), 0.0); } #[test] fn price_add_saturating() { let a = Price::from_f64(100.0).unwrap(); let b = Price::from_f64(200.0).unwrap(); let c = a + b; let diff = (c.to_f64() - 300.0).abs(); assert!(diff < 1e-6); } #[test] fn price_sub_saturating() { let a = Price::from_f64(100.0).unwrap(); let b = Price::from_f64(300.0).unwrap(); // i64 saturating_sub: 100M - 300M = -200M (allowed, i64 is signed) let c = a - b; let diff = (c.to_f64() - (-200.0)).abs(); assert!(diff < 1e-6); } #[test] fn price_mul_i64() { let p = Price::from_f64(50.0).unwrap(); let result = p * 3; let diff = (result.to_f64() - 150.0).abs(); assert!(diff < 1e-6); } #[test] fn price_div_i64() { let p = Price::from_f64(150.0).unwrap(); let result = p / 3; let diff = (result.to_f64() - 50.0).abs(); assert!(diff < 1e-6); } #[test] fn price_div_by_zero_returns_zero() { let p = Price::from_f64(100.0).unwrap(); assert_eq!((p / 0_i64).to_f64(), 0.0); } #[test] fn price_from_decimal_roundtrip() { let d = Decimal::new(123_456_789, 6); // 123.456789 let p = Price::from_decimal(d); let back = p.to_decimal(); assert_eq!(d, back); } #[test] fn price_display() { let p = Price::from_f64(123.456789).unwrap(); let s = format!("{p}"); assert!(s.starts_with("123.456")); } #[test] fn price_from_str() { let p: Price = "123.456".parse().unwrap(); let diff = (p.to_f64() - 123.456).abs(); assert!(diff < 1e-6); } #[test] fn price_ord() { let a = Price::from_f64(100.0).unwrap(); let b = Price::from_f64(200.0).unwrap(); assert!(a < b); } #[test] fn price_abs() { let p = Price((-100 * Price::SCALE) as i64); assert_eq!(p.abs().to_f64(), 100.0); } #[test] fn price_to_f32() { let p = Price::from_f64(123.456).unwrap(); let f = p.to_f32(); assert!((f - 123.456_f32).abs() < 0.01); } #[test] fn price_from_f32() { let p = Price::from_f32(123.456_f32).unwrap(); let diff = (p.to_f64() - 123.456).abs(); assert!(diff < 0.01); // f32 precision } } ``` **Step 2: Write the Price implementation** ```rust //! Canonical financial types with fixed-point i64 arithmetic (6 decimal places). //! //! These types enforce type safety between different financial quantities: //! - `Price` — bid, ask, mid, OHLCV, fill price //! - `Quantity` — contracts, shares, lots (signed for short positions) //! - `Money` — P&L, margin, notional, fees //! - `Ratio` — returns, volatility, Sharpe (f64 newtype with NaN/Inf rejection) use std::fmt; use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use std::str::FromStr; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use crate::types::CommonTypeError; /// Fixed-point scale factor: 6 decimal places pub const SCALE: i64 = 1_000_000; // ============================================================================ // Price // ============================================================================ /// Exact-arithmetic price type using i64 with 6 decimal places. /// /// Represents: bid, ask, mid, last, VWAP, limit, stop, fill, OHLCV. /// Signed to support price deltas and negative mark-to-market. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Price(i64); impl Price { pub const ZERO: Self = Self(0); pub const SCALE: i64 = SCALE; /// Create from f64. Rejects negative, NaN, Inf. #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn from_f64(value: f64) -> Result { if !value.is_finite() || value < 0.0 { return Err(CommonTypeError::InvalidPrice { value: value.to_string(), reason: "Price must be non-negative and finite".to_owned(), }); } Ok(Self((value * SCALE as f64).round() as i64)) } /// Create from f32 (ML boundary). pub fn from_f32(value: f32) -> Result { Self::from_f64(f64::from(value)) } /// Convert to f64 for display or math. #[must_use] #[allow(clippy::cast_precision_loss)] pub fn to_f64(self) -> f64 { self.0 as f64 / SCALE as f64 } /// Convert to f32 (ML tensor input). #[must_use] #[allow(clippy::cast_precision_loss)] pub fn to_f32(self) -> f32 { self.to_f64() as f32 } /// Convert to Decimal (DB/serialization boundary). #[must_use] pub fn to_decimal(self) -> Decimal { Decimal::new(self.0, 6) } /// Create from Decimal. #[must_use] #[allow(clippy::arithmetic_side_effects)] pub fn from_decimal(decimal: Decimal) -> Self { let scaled = decimal * Decimal::new(SCALE, 0); Self(i64::try_from(scaled.mantissa()).unwrap_or(0)) } /// Alias for `from_f64`. pub fn new(value: f64) -> Result { Self::from_f64(value) } /// Raw i64 value (scaled). #[must_use] pub const fn raw_value(self) -> i64 { self.0 } /// Create from raw scaled i64. #[must_use] pub const fn from_raw(value: i64) -> Self { Self(value) } /// Absolute value. #[must_use] pub fn abs(self) -> Self { Self(self.0.saturating_abs()) } /// Square root (for volatility on price diffs). #[must_use] #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn sqrt(self) -> Self { let f = (self.0 as f64 / SCALE as f64).sqrt() * SCALE as f64; Self(f as i64) } #[must_use] pub const fn is_zero(self) -> bool { self.0 == 0 } #[must_use] pub const fn is_positive(self) -> bool { self.0 > 0 } #[must_use] pub const fn is_negative(self) -> bool { self.0 < 0 } } impl Default for Price { fn default() -> Self { Self::ZERO } } impl fmt::Display for Price { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.6}", self.to_f64()) } } impl FromStr for Price { type Err = CommonTypeError; fn from_str(s: &str) -> Result { let v = s.parse::().map_err(|e| CommonTypeError::InvalidPrice { value: s.to_owned(), reason: format!("Cannot parse as price: {e}"), })?; Self::from_f64(v) } } impl Add for Price { type Output = Self; fn add(self, rhs: Self) -> Self { Self(self.0.saturating_add(rhs.0)) } } impl AddAssign for Price { fn add_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_add(rhs.0); } } impl Sub for Price { type Output = Self; fn sub(self, rhs: Self) -> Self { Self(self.0.saturating_sub(rhs.0)) } } impl SubAssign for Price { fn sub_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_sub(rhs.0); } } impl Mul for Price { type Output = Self; fn mul(self, rhs: i64) -> Self { Self(self.0.saturating_mul(rhs)) } } impl Div for Price { type Output = Self; fn div(self, rhs: i64) -> Self { if rhs == 0 { Self(0) } else { Self(self.0.saturating_div(rhs)) } } } impl Neg for Price { type Output = Self; fn neg(self) -> Self { Self(self.0.saturating_neg()) } } // f64 arithmetic (returns Result to prevent silent precision loss) impl Mul for Price { type Output = Result; #[allow(clippy::float_arithmetic)] fn mul(self, rhs: f64) -> Self::Output { Self::from_f64(self.to_f64() * rhs) } } impl Div for Price { type Output = Result; #[allow(clippy::float_arithmetic)] fn div(self, rhs: f64) -> Self::Output { if rhs == 0.0 { return Err(CommonTypeError::ConversionError { message: "Division by zero".to_owned(), }); } Self::from_f64(self.to_f64() / rhs) } } // Decimal interop impl From for Price { fn from(d: Decimal) -> Self { Self::from_decimal(d) } } impl From for Decimal { fn from(p: Price) -> Self { p.to_decimal() } } // Comparisons with f64 (for migration ease) impl PartialEq for Price { fn eq(&self, other: &f64) -> bool { (self.to_f64() - other).abs() < 1e-6 } } impl PartialOrd for Price { fn partial_cmp(&self, other: &f64) -> Option { self.to_f64().partial_cmp(other) } } ``` **Step 3: Run tests** Run: `SQLX_OFFLINE=true cargo test -p common --lib financial_types -- --nocapture` Expected: All 17 tests PASS **Step 4: Wire into common/src/lib.rs** Add `pub mod financial_types;` and add Price to the re-export block. Do NOT remove the old `types::Price` yet — that breaks all downstream. We'll add the new module alongside, then migrate consumers, then delete the old. **Step 5: Commit** ```bash git add crates/common/src/financial_types.rs crates/common/src/lib.rs git commit -m "feat(common): add Price newtype (i64/6dp) in financial_types module" ``` --- ### Task 1.2: Add Quantity to financial_types **Files:** - Modify: `crates/common/src/financial_types.rs` **Step 1: Write the failing tests** Append to the `tests` module in `financial_types.rs`: ```rust #[test] fn quantity_from_f64_roundtrip() { let q = Quantity::from_f64(10.5).unwrap(); let diff = (q.to_f64() - 10.5).abs(); assert!(diff < 1e-6); } #[test] fn quantity_negative_for_short() { let q = Quantity::from_i64(-5); assert!(q.is_negative()); assert_eq!(q.to_f64(), -5.0); } #[test] fn quantity_from_f64_rejects_nan() { assert!(Quantity::from_f64(f64::NAN).is_err()); } #[test] fn quantity_add_sub() { let a = Quantity::from_i64(10); let b = Quantity::from_i64(3); assert_eq!((a + b).raw_value(), 13 * SCALE); assert_eq!((a - b).raw_value(), 7 * SCALE); } #[test] fn quantity_abs() { let q = Quantity::from_i64(-5); assert_eq!(q.abs().to_f64(), 5.0); } ``` **Step 2: Write the Quantity implementation** ```rust // ============================================================================ // Quantity // ============================================================================ /// Exact-arithmetic quantity type using i64 with 6 decimal places. /// /// Represents: contracts, shares, lots. Signed for short positions. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Quantity(i64); impl Quantity { pub const ZERO: Self = Self(0); pub const SCALE: i64 = SCALE; #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn from_f64(value: f64) -> Result { if !value.is_finite() { return Err(CommonTypeError::InvalidQuantity { value: value.to_string(), reason: "Quantity must be finite".to_owned(), }); } Ok(Self((value * SCALE as f64).round() as i64)) } /// Create from whole units (e.g., 5 contracts = from_i64(5)). #[must_use] pub const fn from_i64(value: i64) -> Self { Self(value.saturating_mul(SCALE)) } /// Create from raw scaled value. #[must_use] pub const fn from_raw(value: i64) -> Self { Self(value) } #[must_use] #[allow(clippy::cast_precision_loss)] pub fn to_f64(self) -> f64 { self.0 as f64 / SCALE as f64 } #[must_use] #[allow(clippy::cast_precision_loss)] pub fn to_f32(self) -> f32 { self.to_f64() as f32 } #[must_use] pub fn to_decimal(self) -> Decimal { Decimal::new(self.0, 6) } #[must_use] pub const fn raw_value(self) -> i64 { self.0 } #[must_use] pub fn abs(self) -> Self { Self(self.0.saturating_abs()) } #[must_use] pub const fn is_zero(self) -> bool { self.0 == 0 } #[must_use] pub const fn is_positive(self) -> bool { self.0 > 0 } #[must_use] pub const fn is_negative(self) -> bool { self.0 < 0 } } impl Default for Quantity { fn default() -> Self { Self::ZERO } } impl fmt::Display for Quantity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.6}", self.to_f64()) } } impl Add for Quantity { type Output = Self; fn add(self, rhs: Self) -> Self { Self(self.0.saturating_add(rhs.0)) } } impl Sub for Quantity { type Output = Self; fn sub(self, rhs: Self) -> Self { Self(self.0.saturating_sub(rhs.0)) } } impl Mul for Quantity { type Output = Self; fn mul(self, rhs: i64) -> Self { Self(self.0.saturating_mul(rhs)) } } impl Div for Quantity { type Output = Self; fn div(self, rhs: i64) -> Self { if rhs == 0 { Self(0) } else { Self(self.0.saturating_div(rhs)) } } } impl Neg for Quantity { type Output = Self; fn neg(self) -> Self { Self(self.0.saturating_neg()) } } impl AddAssign for Quantity { fn add_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_add(rhs.0); } } impl SubAssign for Quantity { fn sub_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_sub(rhs.0); } } ``` **Step 3: Run tests** Run: `SQLX_OFFLINE=true cargo test -p common --lib financial_types -- --nocapture` Expected: All 22 tests PASS **Step 4: Commit** ```bash git add crates/common/src/financial_types.rs git commit -m "feat(common): add Quantity newtype (i64/6dp, signed for shorts)" ``` --- ### Task 1.3: Add Money to financial_types **Files:** - Modify: `crates/common/src/financial_types.rs` **Step 1: Write the failing tests** ```rust #[test] fn money_from_f64_roundtrip() { let m = Money::from_f64(-1234.56).unwrap(); let diff = (m.to_f64() - (-1234.56)).abs(); assert!(diff < 1e-6); } #[test] fn money_allows_negative() { let m = Money::from_f64(-500.0).unwrap(); assert!(m.is_negative()); } #[test] fn money_rejects_nan() { assert!(Money::from_f64(f64::NAN).is_err()); } #[test] fn money_add_sub() { let a = Money::from_f64(100.0).unwrap(); let b = Money::from_f64(250.0).unwrap(); let diff = ((a + b).to_f64() - 350.0).abs(); assert!(diff < 1e-6); } #[test] fn price_times_quantity_is_money() { let p = Price::from_f64(100.50).unwrap(); let q = Quantity::from_i64(10); let m: Money = p * q; let diff = (m.to_f64() - 1005.0).abs(); assert!(diff < 1e-6); } #[test] fn money_div_quantity_is_price() { let m = Money::from_f64(1005.0).unwrap(); let q = Quantity::from_i64(10); let p: Price = m / q; let diff = (p.to_f64() - 100.5).abs(); assert!(diff < 1e-6); } ``` **Step 2: Write the Money implementation and cross-type ops** ```rust // ============================================================================ // Money // ============================================================================ /// Exact-arithmetic money type using i64 with 6 decimal places. /// /// Represents: P&L, margin, notional value, fees, commissions, balance. /// Always signed (losses are negative). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct Money(i64); impl Money { pub const ZERO: Self = Self(0); pub const SCALE: i64 = SCALE; #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn from_f64(value: f64) -> Result { if !value.is_finite() { return Err(CommonTypeError::ConversionError { message: format!("Money must be finite, got {value}"), }); } Ok(Self((value * SCALE as f64).round() as i64)) } #[must_use] pub const fn from_raw(value: i64) -> Self { Self(value) } #[must_use] #[allow(clippy::cast_precision_loss)] pub fn to_f64(self) -> f64 { self.0 as f64 / SCALE as f64 } #[must_use] pub fn to_decimal(self) -> Decimal { Decimal::new(self.0, 6) } #[must_use] pub const fn raw_value(self) -> i64 { self.0 } #[must_use] pub fn abs(self) -> Self { Self(self.0.saturating_abs()) } #[must_use] pub const fn is_zero(self) -> bool { self.0 == 0 } #[must_use] pub const fn is_positive(self) -> bool { self.0 > 0 } #[must_use] pub const fn is_negative(self) -> bool { self.0 < 0 } } impl Default for Money { fn default() -> Self { Self::ZERO } } impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.6}", self.to_f64()) } } impl Add for Money { type Output = Self; fn add(self, rhs: Self) -> Self { Self(self.0.saturating_add(rhs.0)) } } impl Sub for Money { type Output = Self; fn sub(self, rhs: Self) -> Self { Self(self.0.saturating_sub(rhs.0)) } } impl Mul for Money { type Output = Self; fn mul(self, rhs: i64) -> Self { Self(self.0.saturating_mul(rhs)) } } impl Div for Money { type Output = Self; fn div(self, rhs: i64) -> Self { if rhs == 0 { Self(0) } else { Self(self.0.saturating_div(rhs)) } } } impl Neg for Money { type Output = Self; fn neg(self) -> Self { Self(self.0.saturating_neg()) } } impl AddAssign for Money { fn add_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_add(rhs.0); } } impl SubAssign for Money { fn sub_assign(&mut self, rhs: Self) { self.0 = self.0.saturating_sub(rhs.0); } } // ============================================================================ // Cross-type arithmetic // ============================================================================ /// Price * Quantity = Money (notional value) impl Mul for Price { type Output = Money; #[allow(clippy::cast_precision_loss)] fn mul(self, rhs: Quantity) -> Money { // (price_scaled * qty_scaled) / SCALE = money_scaled // Use i128 to avoid overflow in intermediate product let product = (self.0 as i128).saturating_mul(rhs.0 as i128); let money_scaled = product / SCALE as i128; Money(i64::try_from(money_scaled.clamp(i64::MIN as i128, i64::MAX as i128)).unwrap_or(0)) } } /// Money / Quantity = Price (average price) impl Div for Money { type Output = Price; fn div(self, rhs: Quantity) -> Price { if rhs.0 == 0 { Price::ZERO } else { let result = (self.0 as i128).saturating_mul(SCALE as i128) / rhs.0 as i128; Price(i64::try_from(result.clamp(i64::MIN as i128, i64::MAX as i128)).unwrap_or(0)) } } } ``` **Step 3: Run tests** Run: `SQLX_OFFLINE=true cargo test -p common --lib financial_types -- --nocapture` Expected: All 28 tests PASS **Step 4: Commit** ```bash git add crates/common/src/financial_types.rs git commit -m "feat(common): add Money newtype and cross-type arithmetic (Price * Qty = Money)" ``` --- ### Task 1.4: Add Ratio to financial_types **Files:** - Modify: `crates/common/src/financial_types.rs` **Step 1: Write the failing tests** ```rust #[test] fn ratio_new_rejects_nan() { assert!(Ratio::new(f64::NAN).is_err()); } #[test] fn ratio_new_rejects_inf() { assert!(Ratio::new(f64::INFINITY).is_err()); } #[test] fn ratio_new_allows_negative() { let r = Ratio::new(-0.5).unwrap(); assert!(r.is_negative()); } #[test] fn ratio_arithmetic() { let a = Ratio::new(0.5).unwrap(); let b = Ratio::new(0.3).unwrap(); let sum = a + b; assert!((sum.to_f64() - 0.8).abs() < 1e-10); } #[test] fn ratio_sqrt() { let r = Ratio::new(4.0).unwrap(); assert!((r.sqrt().to_f64() - 2.0).abs() < 1e-10); } #[test] fn ratio_ln_exp_roundtrip() { let r = Ratio::new(2.5).unwrap(); let back = r.ln().exp(); assert!((back.to_f64() - 2.5).abs() < 1e-10); } #[test] fn ratio_clamp() { let r = Ratio::new(1.5).unwrap(); let clamped = r.clamp(0.0, 1.0); assert_eq!(clamped.to_f64(), 1.0); } #[test] fn ratio_mul_price_scales() { let p = Price::from_f64(100.0).unwrap(); let r = Ratio::new(0.05).unwrap(); // 5% return let result = p.scale_by(r); let diff = (result.to_f64() - 5.0).abs(); assert!(diff < 1e-6); } ``` **Step 2: Write the Ratio implementation** ```rust // ============================================================================ // Ratio // ============================================================================ /// NaN/Inf-rejecting f64 newtype for continuous mathematical quantities. /// /// Represents: returns, volatility, Sharpe, correlation, Kelly fraction, /// feature importance, confidence scores. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Ratio(f64); impl Ratio { pub const ZERO: Self = Self(0.0); pub const ONE: Self = Self(1.0); /// Create a Ratio, rejecting NaN and Inf. pub fn new(value: f64) -> Result { if !value.is_finite() { return Err(CommonTypeError::ConversionError { message: format!("Ratio must be finite, got {value}"), }); } Ok(Self(value)) } /// Create without validation (for const contexts and trusted internal use). #[must_use] pub const fn from_raw(value: f64) -> Self { Self(value) } #[must_use] pub fn to_f64(self) -> f64 { self.0 } #[must_use] #[allow(clippy::cast_possible_truncation)] pub fn to_f32(self) -> f32 { self.0 as f32 } #[must_use] pub fn abs(self) -> Self { Self(self.0.abs()) } #[must_use] pub fn sqrt(self) -> Self { Self(self.0.sqrt()) } #[must_use] pub fn ln(self) -> Self { Self(self.0.ln()) } #[must_use] pub fn exp(self) -> Self { Self(self.0.exp()) } #[must_use] pub fn powi(self, n: i32) -> Self { Self(self.0.powi(n)) } #[must_use] pub fn is_positive(self) -> bool { self.0 > 0.0 } #[must_use] pub fn is_negative(self) -> bool { self.0 < 0.0 } #[must_use] pub fn clamp(self, min: f64, max: f64) -> Self { Self(self.0.clamp(min, max)) } } impl Default for Ratio { fn default() -> Self { Self::ZERO } } impl fmt::Display for Ratio { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl Add for Ratio { type Output = Self; fn add(self, rhs: Self) -> Self { Self(self.0 + rhs.0) } } impl Sub for Ratio { type Output = Self; fn sub(self, rhs: Self) -> Self { Self(self.0 - rhs.0) } } impl Mul for Ratio { type Output = Self; fn mul(self, rhs: Self) -> Self { Self(self.0 * rhs.0) } } impl Div for Ratio { type Output = Self; fn div(self, rhs: Self) -> Self { if rhs.0 == 0.0 { Self(0.0) } else { Self(self.0 / rhs.0) } } } impl Neg for Ratio { type Output = Self; fn neg(self) -> Self { Self(-self.0) } } impl AddAssign for Ratio { fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; } } impl SubAssign for Ratio { fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0; } } // ============================================================================ // Price * Ratio scaling // ============================================================================ impl Price { /// Scale a price by a ratio (e.g., price * 5% return). #[must_use] #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn scale_by(self, ratio: Ratio) -> Self { Self((self.0 as f64 * ratio.0).round() as i64) } } ``` **Step 3: Run tests** Run: `SQLX_OFFLINE=true cargo test -p common --lib financial_types -- --nocapture` Expected: All 36 tests PASS **Step 4: Commit** ```bash git add crates/common/src/financial_types.rs git commit -m "feat(common): add Ratio newtype (f64 with NaN/Inf rejection)" ``` --- ### Task 1.5: Wire financial_types into common's public API **Files:** - Modify: `crates/common/src/lib.rs` **Step 1: Add the module declaration and re-exports** Add to `lib.rs`: ```rust pub mod financial_types; // Re-export canonical financial types at crate root pub use financial_types::{ Money as FinancialMoney, Price as FinancialPrice, Quantity as FinancialQuantity, Ratio, SCALE as FINANCIAL_SCALE, }; ``` Note: We use aliased re-exports (`FinancialPrice`, etc.) temporarily to avoid name collision with the existing `types::Price` and `types::Quantity`. Once migration is complete (Phase 2+), we'll rename these to `Price`/`Quantity`/`Money` and delete the old types. **Step 2: Run workspace check** Run: `SQLX_OFFLINE=true cargo check -p common` Expected: PASS (no downstream impact since re-exports are additive) **Step 3: Commit** ```bash git add crates/common/src/lib.rs git commit -m "feat(common): re-export financial_types as FinancialPrice/Quantity/Money/Ratio" ``` --- ## Phase 2: Core Crate Migration (3 parallel worktrees) **Strategy:** Each crate gets its own worktree. All three can run in parallel since they only depend on `common/` (which is done). Each worktree follows the same pattern: 1. Replace `use common::Price` with `use common::FinancialPrice as Price` (or update field types) 2. Replace f64 price/qty/pnl fields with the new types 3. Replace `as f64` casts with `.to_f64()` calls 4. Fix compilation errors 5. Run tests, fix failures 6. Clippy clean ### Task 2.1: Migrate trading_engine/ **Worktree:** `feature/numeric-types-trading-engine` **Files:** - Delete: `crates/trading_engine/src/types/financial.rs` (IntegerPrice/IntegerQuantity/IntegerMoney absorbed into common) - Modify: `crates/trading_engine/src/types/mod.rs` — remove `pub mod financial;` - Modify: All files importing `IntegerPrice`/`IntegerQuantity`/`IntegerMoney` — switch to `common::FinancialPrice`/`FinancialQuantity`/`FinancialMoney` - Modify: Files with `price: f64` fields — switch to `Price` - Modify: `crates/trading_engine/src/types/financial_safe.rs` — update to use new types - Modify: `crates/trading_engine/src/types/tests/financial_tests.rs` — update assertions for new scale/API **Migration pattern per file:** ```rust // BEFORE use crate::types::financial::IntegerPrice; let price = IntegerPrice::from_f64(100.5); let val = price.to_f64(); // AFTER use common::FinancialPrice as Price; let price = Price::from_f64(100.5).unwrap_or(Price::ZERO); let val = price.to_f64(); ``` Key differences to handle: - `IntegerPrice::from_f64` was infallible → new `Price::from_f64` returns `Result` - `IntegerPrice(pub i64)` had public field → new `Price` uses `from_raw()`/`raw_value()` - `PRICE_SCALE` constant → `Price::SCALE` - `IntegerMoney.to_price()` → just use `Price::from_raw(money.raw_value())` **Step 1:** Replace all `IntegerPrice`/`IntegerQuantity`/`IntegerMoney` imports and usages **Step 2:** Replace remaining `price: f64` fields in order types, position types **Step 3:** Run `SQLX_OFFLINE=true cargo check -p trading_engine`, fix errors **Step 4:** Run `SQLX_OFFLINE=true cargo test -p trading_engine --lib`, fix failures **Step 5:** Run `cargo clippy -p trading_engine`, fix warnings **Step 6:** Delete `types/financial.rs` once no imports remain **Step 7:** Commit ```bash git commit -m "refactor(trading_engine): migrate to common financial types, delete IntegerPrice/Quantity/Money" ``` --- ### Task 2.2: Migrate risk/ **Worktree:** `feature/numeric-types-risk` **Files:** - Modify: `crates/risk/src/risk_engine.rs` — 12 `price: f64` fields - Modify: `crates/risk/src/position_tracker.rs` — position price/qty fields - Modify: `crates/risk/src/risk_types.rs` — risk limit types - Modify: `crates/risk/src/safety/position_limiter.rs` - Modify: VaR calculation files (keep f64 for statistical math, use Price for price inputs/outputs) **Migration pattern:** ```rust // BEFORE pub struct PositionInfo { pub avg_price: f64, pub quantity: f64, pub unrealized_pnl: f64, } // AFTER use common::{FinancialPrice as Price, FinancialQuantity as Quantity, FinancialMoney as Money}; pub struct PositionInfo { pub avg_price: Price, pub quantity: Quantity, pub unrealized_pnl: Money, } ``` **Key:** VaR/Kelly calculations use `sqrt`, `ln`, `exp` — these stay as `Ratio` or raw f64 internally. Only the input prices and output dollar amounts use Price/Money. **Step 1:** Replace f64 fields in structs with Price/Quantity/Money **Step 2:** Update method signatures and implementations **Step 3:** Run `SQLX_OFFLINE=true cargo check -p risk`, fix errors **Step 4:** Run `SQLX_OFFLINE=true cargo test -p risk --lib`, fix failures **Step 5:** Clippy clean **Step 6:** Commit ```bash git commit -m "refactor(risk): migrate price/qty/pnl fields to financial types" ``` --- ### Task 2.3: Migrate data/ **Worktree:** `feature/numeric-types-data` **Files:** - Modify: `crates/data/src/features.rs` — PricePoint (line 538) - Modify: `crates/data/src/validation.rs` — PricePoint (line 234), PriceBounds (line 217), PriceValidator - Modify: `crates/data/src/providers/common.rs` — PriceLevelChange (line 1130) - Modify: `crates/data/src/unified_feature_extractor.rs` — PriceReaction (line 206) - Modify: OHLCV struct fields wherever they use f64 **Migration pattern:** ```rust // BEFORE pub struct PriceBounds { pub min_price: f64, pub max_price: f64, } // AFTER use common::FinancialPrice as Price; pub struct PriceBounds { pub min_price: Price, pub max_price: Price, } ``` **Step 1:** Replace f64 price fields in all data structs **Step 2:** Update validation logic (comparisons use Price ordering) **Step 3:** Run `SQLX_OFFLINE=true cargo check -p data`, fix errors **Step 4:** Run `SQLX_OFFLINE=true cargo test -p data --lib`, fix failures **Step 5:** Clippy clean **Step 6:** Commit ```bash git commit -m "refactor(data): migrate price fields to financial types" ``` --- ## Phase 3: ML Boundary (1 worktree) ### Task 3.1: Migrate ml/ price-adjacent code **Worktree:** `feature/numeric-types-ml` **Files to migrate** (price fields only — NOT weights/gradients/losses): - `crates/ml/src/labeling/concurrent_tracking.rs:18` — PricePoint.price_cents - `crates/ml/src/labeling/triple_barrier.rs:21` — PricePoint.price_cents - `crates/ml/src/features/price_features.rs` — price input/output - `crates/ml/src/features/microstructure_features.rs:691` — PriceImpact - `crates/ml/src/universe/volatility.rs:84` — PricePoint - `crates/ml/src/ensemble/adaptive_ml_integration.rs:92` — PricePoint.price - `crates/ml/src/training/unified_data_loader.rs:148` — PriceData (already uses Price!) **Files to NOT migrate** (these use f64/f32 correctly): - All `src/dqn/`, `src/ppo/`, `src/trainers/` — weights, gradients, losses - All `src/models/` — neural network internals - Tensor operations — Candle requires f32/f64 **Boundary pattern:** ```rust // ML model output → Price let prediction_f32: f32 = model.forward(&input)?; let predicted_price = Price::from_f32(prediction_f32)?; // Price → ML model input let prices: Vec = market_data.prices(); let tensor_data: Vec = prices.iter().map(|p| p.to_f32()).collect(); let input = Tensor::from_vec(tensor_data, &[batch, seq_len], device)?; ``` **Step 1:** Replace f64 price fields in labeling/features structs **Step 2:** Add `.to_f32()` / `Price::from_f32()` at tensor boundaries **Step 3:** Run `SQLX_OFFLINE=true cargo check -p ml`, fix errors **Step 4:** Run `SQLX_OFFLINE=true cargo test -p ml --lib`, fix failures (2390 tests) **Step 5:** Clippy clean **Step 6:** Commit ```bash git commit -m "refactor(ml): migrate price fields to financial types, add tensor boundary conversions" ``` --- ## Phase 4: Services (4 parallel worktrees) Each service follows the identical migration pattern. All four can run in parallel. ### Task 4.1: Migrate trading_service/ **Worktree:** `feature/numeric-types-trading-service` **Key files:** - `services/trading_service/src/assets.rs` — asset price fields - `services/trading_service/src/core/position_manager.rs` — 10 price: f64 fields - `services/trading_service/src/core/risk_manager.rs` - `services/trading_service/src/services/enhanced_ml.rs` - `services/trading_service/src/ensemble_risk_manager.rs` - `services/trading_service/src/paper_trading_executor.rs` - `services/trading_service/src/utils.rs` - Proto conversion boundary: `f64 ↔ Price` at gRPC handler layer **Step 1:** Replace f64 fields → Price/Quantity/Money **Step 2:** Add proto boundary conversions (`price.to_f64()` in response, `Price::from_f64(req.price)?` in handler) **Step 3:** Run `SQLX_OFFLINE=true cargo check -p trading_service`, fix errors **Step 4:** Run `SQLX_OFFLINE=true cargo test -p trading_service --lib`, fix failures (211 tests) **Step 5:** Clippy clean **Step 6:** Commit ### Task 4.2: Migrate backtesting_service/ **Worktree:** `feature/numeric-types-backtesting-service` Same pattern. Key files: `dbn_data_source.rs`, strategy engine, equity curve tracking. ### Task 4.3: Migrate trading_agent_service/ **Worktree:** `feature/numeric-types-trading-agent-service` Same pattern. Key files: `dynamic_stop_loss.rs`, `orders.rs`, `assets.rs`. ### Task 4.4: Migrate remaining services **Worktree:** `feature/numeric-types-remaining-services` Covers: `api_gateway`, `broker_gateway_service`, `ml_training_service`, `data_acquisition_service`. --- ## Phase 5: Peripheral (2 parallel worktrees) ### Task 5.1: Migrate bin/fxt/ and web-gateway/ **Worktree:** `feature/numeric-types-fxt-gateway` **Key files:** - `bin/fxt/src/types.rs` — CLI types with price: f64 - `crates/web-gateway/src/routes/trading.rs` — order validation - Proto boundary: gRPC ↔ Price conversions ### Task 5.2: Migrate testing/ crates **Worktree:** `feature/numeric-types-testing` **Key files:** - `testing/integration/fixtures/` — test data builders - `testing/integration/unit/financial_property_tests.rs` — has its own `Quantity(u64)` struct - `testing/test-common/lib.rs` — test helpers - `testing/e2e/` — end-to-end test types --- ## Phase 6: Cleanup (1 worktree, after all merges) ### Task 6.1: Delete old types and rename re-exports **Files:** - Modify: `crates/common/src/types.rs` — delete old `Price` struct (lines 2254-2675), old `Quantity` struct (lines 2677-end) - Modify: `crates/common/src/trading.rs` — delete duplicate `Quantity` struct (lines 100-145+) - Modify: `crates/common/src/lib.rs` — rename `FinancialPrice` → `Price`, `FinancialQuantity` → `Quantity`, `FinancialMoney` → `Money` in re-exports - Modify: All consumers — remove `as Price` aliases, use `common::Price` directly **Step 1:** Delete old Price/Quantity from types.rs **Step 2:** Delete duplicate Quantity from trading.rs **Step 3:** Update lib.rs re-exports to canonical names **Step 4:** Run `SQLX_OFFLINE=true cargo check --workspace`, fix all errors **Step 5:** Run `SQLX_OFFLINE=true cargo test --workspace --lib` (4500+ tests) **Step 6:** Clippy workspace clean **Step 7:** Commit ```bash git commit -m "refactor(common): delete legacy Price/Quantity types, promote financial_types to canonical names" ``` ### Task 6.2: Remove dead bigdecimal dependency **Files:** - Modify: workspace `Cargo.toml` — remove `bigdecimal` from `[workspace.dependencies]` - Modify: any crate Cargo.toml still referencing it (check sqlx features) **Step 1:** Remove bigdecimal where unused **Step 2:** Verify sqlx still compiles (it may need `rust_decimal` feature but not `bigdecimal`) **Step 3:** `cargo check --workspace` **Step 4:** Commit ```bash git commit -m "chore: remove unused bigdecimal dependency" ``` --- ## Verification Checklist (run after all phases merged) ```bash # Full workspace compile SQLX_OFFLINE=true cargo check --workspace # Full test suite SQLX_OFFLINE=true cargo test --workspace --lib # Clippy clean SQLX_OFFLINE=true cargo clippy --workspace -- -D warnings # Verify no raw f64 price fields remain (should be 0 or near-0) rg 'price:\s*f64' crates/ services/ --type rust | wc -l # Verify IntegerPrice is gone rg 'IntegerPrice|IntegerQuantity|IntegerMoney' crates/ services/ --type rust | wc -l # Verify old Price/Quantity scale (100_000_000) is gone rg '100_000_000' crates/common/src/types.rs | wc -l ``` --- ## Worktree Swarm Summary | Phase | Worktrees | Can Parallel? | Depends On | |-------|-----------|---------------|------------| | 1 | 1 (foundation) | — | Nothing | | 2 | 3 (trading_engine, risk, data) | Yes, all 3 | Phase 1 | | 3 | 1 (ml) | — | Phase 1 | | 4 | 4 (services) | Yes, all 4 | Phases 1-3 | | 5 | 2 (fxt+gateway, testing) | Yes, both | Phases 1-4 | | 6 | 1 (cleanup) | — | All above | **Total:** 12 worktrees, max 4 concurrent. Phases 2+3 can run together (4 worktrees). Phase 4 runs 4 worktrees. Phase 5 runs 2.