Consolidate competing Price/Quantity implementations into canonical i64 fixed-point types in common/. Adds Money and Ratio newtypes. Phased migration plan across 5 phases using parallel worktrees. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.3 KiB
Numeric Type Standardization Design
Date: 2026-02-28 Status: Approved Goal: Eliminate precision-lossy f64 casting by standardizing on domain-typed i64 fixed-point wrappers for all financial values
Problem
- 23,730
ascasts across 1,044 files — many are f32↔f64 conversions at ML↔trading boundaries - 11,000+ raw f64 occurrences for prices, quantities, P&L — no type safety between different financial quantities
- Two competing implementations:
common::Price(u64, 8dp) vstrading_engine::IntegerPrice(i64, 6dp) - Neither widely adopted — raw
price: f64still dominates (88+ field occurrences) - Proactive hardening: prevent precision bugs before they affect P&L or risk calculations
Decision
Approach C: Domain-typed wrappers with i64 fixed-point internals (6 decimal places)
Rejected alternatives:
- rust_decimal::Decimal everywhere: 3-10x slower than f64, no sqrt/ln/exp, kills SIMD optimizations in trading_engine
- Fixed-point tick integers: Correct for HFT but requires per-instrument tick tables and is too large an architectural change
- u64 Price (current common::Price): Cannot represent negative values (P&L, unrealized losses)
Canonical Type Set
All types live in crates/common/src/ and are re-exported from common::.
Price (i64, scale 1_000_000)
Represents: bid, ask, mid, last, VWAP, limit price, stop price, fill price, OHLCV values.
#[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 = 1_000_000;
pub fn from_f64(value: f64) -> Result<Self, CommonTypeError>;
pub fn to_f64(self) -> f64;
pub fn to_f32(self) -> f32; // ML boundary only
pub fn from_f32(value: f32) -> Result<Self, CommonTypeError>;
pub fn to_decimal(self) -> Decimal;
pub fn from_decimal(decimal: Decimal) -> Self;
pub fn abs(self) -> Self;
pub fn sqrt(self) -> Self; // For volatility calculations on price diffs
}
6 decimal places sufficient for: ES (0.25), NQ (0.25), 6E (0.00005), ZN (1/64=0.015625), FX spot (0.00001), equities ($0.0001).
Quantity (i64, scale 1_000_000)
Represents: contracts, shares, lots, position sizes. Signed to represent short positions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Quantity(i64);
Money (i64, scale 1_000_000)
Represents: P&L, margin, notional value, fees, commissions, account balance. Always signed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Money(i64);
Ratio (f64 newtype)
Represents: returns, percentages, volatility, Sharpe ratio, correlation, Kelly fraction — continuous mathematical quantities where IEEE 754 is correct.
#[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);
pub fn new(value: f64) -> Result<Self, CommonTypeError>; // rejects NaN/Inf
pub fn to_f64(self) -> f64;
pub fn to_f32(self) -> f32; // ML boundary
pub fn abs(self) -> Self;
pub fn sqrt(self) -> Self;
pub fn ln(self) -> Self;
pub fn exp(self) -> Self;
pub fn powi(self, n: i32) -> Self;
pub fn is_positive(self) -> bool;
pub fn is_negative(self) -> bool;
pub fn clamp(self, min: f64, max: f64) -> Self;
}
Benefits of newtype over alias:
- NaN/Inf rejection at construction — invalid values caught at boundaries, not mid-calculation
- Type safety — can't accidentally assign a Ratio to a Price field
- Audit trail — all f64→Ratio conversions go through
Ratio::new()which validates finiteness - Implements standard math ops (Add, Sub, Mul, Div, Neg) delegating to f64 arithmetic
Arithmetic
All types use saturating arithmetic (no panics on overflow):
Price + Price → Price(saturating_add)Price - Price → Price(saturating_sub)Price * Quantity → Money(cross-type multiplication)Money / Quantity → Price(cross-type division)Price * i64 → Price(scaling)Price / i64 → Price(splitting)- Division by zero returns
Self(0)(consistent with existing IntegerPrice behavior)
Type Boundaries
┌─────────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ FINANCIAL DOMAIN │ │ ML DOMAIN │ │ ANALYTICS │
│ │ │ │ │ │
│ Price(i64) │────▶│ f32 tensors │ │ Ratio (f64) │
│ Quantity(i64) │ │ (Candle GPU) │────▶│ (returns, vol, │
│ Money(i64) │◀────│ predictions f32 │ │ Sharpe, stats) │
│ │ │ │ │ │
└─────────────────────┘ └──────────────────┘ └─────────────────┘
Conversion rules
Price → f64: explicit.to_f64()onlyf64 → Price: explicitPrice::from_f64()(fallible, rounds to scale)Price → f32: explicit.to_f32()(ML tensor input)f32 → Price: explicitPrice::from_f32()(ML prediction output)- No
impl From<f64> for Price— all conversions are explicit and auditable - No implicit casting — compiler enforces type boundaries
What stays raw f64/f32
- ML weights, gradients, losses, learning rates, activations (Candle tensor internals)
- Raw sensor/timing data (RDTSC timestamps, latency measurements)
What becomes Ratio
- Returns, log-returns, percentage changes
- Volatility, implied vol, realized vol
- Sharpe ratio, Sortino, Calmar, information ratio
- Correlation coefficients, beta, R-squared
- Kelly fraction, position sizing multipliers
- Feature importance scores, confidence scores
- Any continuous mathematical quantity that is not a financial amount
Migration Strategy
Bottom-up, dependency-driven, parallelized via git worktrees.
Phase 1: Foundation (1 worktree)
Crate: common/
- Consolidate
Price/Quantityinto i64/6dp (adopt IntegerPrice's API) - Delete the old u64
Pricetype - Add
Moneytype (i64/6dp) - Add
type Ratio = f64 - Implement all arithmetic ops (Add, Sub, Mul, Div, Display, FromStr, Serialize)
- Implement cross-type ops (
Price * Quantity → Money) - Add Decimal interop (to_decimal, from_decimal)
- Update all
common/internal usage - All downstream crates will fail to compile — that's expected and intentional
Phase 2: Core crates (3 parallel worktrees)
Crates: trading_engine/, risk/, data/
trading_engine/: deletetypes/financial.rs(IntegerPrice/IntegerQuantity/IntegerMoney), usecommon::Price/Quantity/Moneyeverywhererisk/: replace f64 price/pnl/margin fields with Price/Money in risk_engine.rs, position_tracker.rs, risk_types.rs, VaR calculationsdata/: replace f64 in OHLCV structs, PricePoint, PriceBounds, validation, feature extraction
Phase 3: ML boundary (1 worktree)
Crate: ml/
- Replace f64 price fields in labeling (PricePoint, triple_barrier) with Price
- Replace f64 price fields in features (price_features, microstructure) with Price
- Add
.to_f32()/Price::from_f32()at Candle tensor boundaries - Keep f32/f64 for weights, gradients, losses, learning rates (these are NOT prices)
- Keep f64 for ensemble scores, confidence, feature importance (these are Ratios)
Phase 4: Services (4 parallel worktrees)
Crates: trading_service/, backtesting_service/, trading_agent_service/, remaining services
- Replace f64 price/qty/pnl fields in each service
- Update proto type conversions (f64 ↔ Price at gRPC boundary)
- Update sqlx queries (Decimal ↔ Price at DB boundary)
Phase 5: Peripheral (2 parallel worktrees)
Crates: bin/fxt/, web-gateway/, testing/
- Update proto-generated types
- Update API request/response types
- Update test fixtures and builders
Per-crate migration checklist
- Replace
price: f64fields →price: Price - Replace
quantity: f64fields →quantity: Quantity - Replace P&L/margin/notional f64 fields →
Money - Replace
as f64/as f32casts →.to_f64()/.to_f32() - Run
SQLX_OFFLINE=true cargo check -p <crate> - Fix compilation errors
- Run
SQLX_OFFLINE=true cargo test -p <crate> --lib - Run
cargo clippy -p <crate>
Scope Exclusions
- No changes to Candle internals (f32 tensors stay f32)
- No changes to ML training loops (weights/gradients/losses stay f64/f32)
- Ratio is a proper f64 newtype with NaN/Inf rejection at construction
- No per-instrument tick size enforcement (future enhancement)
- No bigdecimal removal (deferred — only used in sqlx features)
Risk Mitigation
- Phased rollout: each phase is independently compilable and testable
- Worktree isolation: parallel work doesn't conflict
- 4,500+ existing tests: catch regressions immediately
- Clippy deny rules:
clippy::as_conversionsalready denied — new types enforce this - Saturating arithmetic: no panics on overflow (consistent with existing IntegerPrice)