# 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<Self, CommonTypeError> {
        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, CommonTypeError> {
        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, CommonTypeError> {
        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<Self, Self::Err> {
        let v = s.parse::<f64>().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<i64> for Price {
    type Output = Self;
    fn mul(self, rhs: i64) -> Self {
        Self(self.0.saturating_mul(rhs))
    }
}

impl Div<i64> 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<f64> for Price {
    type Output = Result<Self, CommonTypeError>;
    #[allow(clippy::float_arithmetic)]
    fn mul(self, rhs: f64) -> Self::Output {
        Self::from_f64(self.to_f64() * rhs)
    }
}

impl Div<f64> for Price {
    type Output = Result<Self, CommonTypeError>;
    #[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<Decimal> for Price {
    fn from(d: Decimal) -> Self {
        Self::from_decimal(d)
    }
}

impl From<Price> for Decimal {
    fn from(p: Price) -> Self {
        p.to_decimal()
    }
}

// Comparisons with f64 (for migration ease)
impl PartialEq<f64> for Price {
    fn eq(&self, other: &f64) -> bool {
        (self.to_f64() - other).abs() < 1e-6
    }
}

impl PartialOrd<f64> for Price {
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
        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<Self, CommonTypeError> {
        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<i64> for Quantity {
    type Output = Self;
    fn mul(self, rhs: i64) -> Self { Self(self.0.saturating_mul(rhs)) }
}

impl Div<i64> 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<Self, CommonTypeError> {
        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<i64> for Money {
    type Output = Self;
    fn mul(self, rhs: i64) -> Self { Self(self.0.saturating_mul(rhs)) }
}

impl Div<i64> 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<Quantity> 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<Quantity> 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<Self, CommonTypeError> {
        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<Price> = market_data.prices();
let tensor_data: Vec<f32> = 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.
