//! Fixed-point execution primitives for money and fee arithmetic. //! //! Market data and analytics remain floating point at their API boundaries. //! The execution kernel quantizes monetary values to micro-yuan before fee, //! budget and cash-ledger arithmetic so repeated fills and external cash flows //! do not accumulate binary floating-point drift. use std::collections::{BTreeMap, VecDeque}; use chrono::NaiveDate; use crate::events::OrderSide; pub const MONEY_SCALE: i128 = 1_000_000; const MONEY_SCALE_F64: f64 = MONEY_SCALE as f64; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct FixedMoney(i128); impl FixedMoney { pub const ZERO: Self = Self(0); pub const fn from_raw(raw: i128) -> Self { Self(raw) } pub const fn raw(self) -> i128 { self.0 } pub fn from_decimal_str(value: &str) -> Result { let value = value.trim(); if value.is_empty() { return Err("fixed money value is empty".to_string()); } let (negative, unsigned) = match value.as_bytes()[0] { b'-' => (true, &value[1..]), b'+' => (false, &value[1..]), _ => (false, value), }; let mut parts = unsigned.split('.'); let whole = parts.next().unwrap_or_default(); let fractional = parts.next().unwrap_or_default(); if parts.next().is_some() || whole.is_empty() || !whole.bytes().all(|byte| byte.is_ascii_digit()) || !fractional.bytes().all(|byte| byte.is_ascii_digit()) { return Err(format!("invalid fixed money decimal: {value}")); } let whole = whole .parse::() .map_err(|_| format!("fixed money whole part is out of range: {value}"))?; let mut fractional_digits = fractional.as_bytes().to_vec(); let round_up = fractional_digits.len() > 6 && fractional_digits[6] >= b'5'; fractional_digits.truncate(6); while fractional_digits.len() < 6 { fractional_digits.push(b'0'); } let fractional = if fractional_digits.is_empty() { 0 } else { std::str::from_utf8(&fractional_digits) .expect("fractional digits are ASCII") .parse::() .map_err(|_| format!("fixed money fractional part is invalid: {value}"))? }; let mut raw = whole .checked_mul(MONEY_SCALE) .and_then(|raw| raw.checked_add(fractional)) .ok_or_else(|| format!("fixed money value is out of range: {value}"))?; if round_up { raw = raw .checked_add(1) .ok_or_else(|| format!("fixed money value is out of range: {value}"))?; } Ok(Self(if negative { -raw } else { raw })) } pub fn from_f64(value: f64) -> Option { if !value.is_finite() { return None; } let raw = (value * MONEY_SCALE_F64).round(); if !raw.is_finite() || raw < i128::MIN as f64 || raw > i128::MAX as f64 { return None; } Some(Self(raw as i128)) } pub fn to_f64(self) -> f64 { self.0 as f64 / MONEY_SCALE_F64 } pub fn checked_add(self, other: Self) -> Option { self.0.checked_add(other.0).map(Self) } pub fn checked_sub(self, other: Self) -> Option { self.0.checked_sub(other.0).map(Self) } pub fn checked_mul_quantity(self, quantity: u64) -> Option { self.0.checked_mul(i128::from(quantity)).map(Self) } pub fn checked_neg(self) -> Option { self.0.checked_neg().map(Self) } pub fn checked_mul_rate(self, rate: Self) -> Option { let product = self.0.checked_mul(rate.0)?; let half = MONEY_SCALE / 2; let rounded = if product >= 0 { product.checked_add(half)? / MONEY_SCALE } else { product.checked_sub(half)? / MONEY_SCALE }; Some(Self(rounded)) } pub fn checked_sum_f64(values: impl IntoIterator) -> Option { values.into_iter().try_fold(Self::ZERO, |total, value| { total.checked_add(Self::from_f64(value)?) }) } pub fn f64_fits_within(value: f64, limit: f64) -> Option { let value = Self::from_f64(value)?; if limit == f64::INFINITY { return Some(true); } Some(value <= Self::from_f64(limit)?) } pub fn abs(self) -> Self { Self(self.0.abs()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct FixedTradingCost { pub commission: FixedMoney, pub stamp_tax: FixedMoney, pub transfer_fee: FixedMoney, } impl FixedTradingCost { pub fn total(self) -> FixedMoney { FixedMoney::from_raw(self.commission.raw() + self.stamp_tax.raw() + self.transfer_fee.raw()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FixedChinaAShareCostModel { pub commission_rate: FixedMoney, pub stamp_tax_rate_before_change: FixedMoney, pub stamp_tax_rate_after_change: FixedMoney, pub stamp_tax_change_date: NaiveDate, pub minimum_commission: FixedMoney, pub transfer_fee_rate: FixedMoney, } impl FixedChinaAShareCostModel { pub fn commission_for(self, gross_amount: FixedMoney) -> FixedMoney { if gross_amount.raw() <= 0 { return FixedMoney::ZERO; } let raw = gross_amount .checked_mul_rate(self.commission_rate) .expect("fixed commission multiplication overflow"); raw.max(self.minimum_commission) } pub fn stamp_tax_rate_for(self, date: NaiveDate) -> FixedMoney { if date < self.stamp_tax_change_date { self.stamp_tax_rate_before_change } else { self.stamp_tax_rate_after_change } } pub fn stamp_tax_for( self, date: NaiveDate, side: OrderSide, gross_amount: FixedMoney, ) -> FixedMoney { if gross_amount.raw() <= 0 || side == OrderSide::Buy { return FixedMoney::ZERO; } gross_amount .checked_mul_rate(self.stamp_tax_rate_for(date)) .expect("fixed stamp tax multiplication overflow") } pub fn transfer_fee_for(self, gross_amount: FixedMoney) -> FixedMoney { if gross_amount.raw() <= 0 { return FixedMoney::ZERO; } gross_amount .checked_mul_rate(self.transfer_fee_rate) .expect("fixed transfer fee multiplication overflow") } pub fn calculate( self, date: NaiveDate, side: OrderSide, gross_amount: FixedMoney, ) -> FixedTradingCost { FixedTradingCost { commission: self.commission_for(gross_amount), stamp_tax: self.stamp_tax_for(date, side, gross_amount), transfer_fee: self.transfer_fee_for(gross_amount), } } pub fn commission_for_order_fill( self, gross_amount: FixedMoney, order_id: Option, commission_state: &mut BTreeMap, ) -> FixedMoney { if gross_amount.raw() <= 0 { return FixedMoney::ZERO; } let raw = gross_amount .checked_mul_rate(self.commission_rate) .expect("fixed commission multiplication overflow"); let Some(order_id) = order_id else { return raw.max(self.minimum_commission); }; let remaining = commission_state .entry(order_id) .or_insert(self.minimum_commission); self.commission_for_order_fill_remaining(gross_amount, remaining) } pub fn commission_for_order_fill_remaining( self, gross_amount: FixedMoney, remaining: &mut FixedMoney, ) -> FixedMoney { if gross_amount.raw() <= 0 { return FixedMoney::ZERO; } let raw = gross_amount .checked_mul_rate(self.commission_rate) .expect("fixed commission multiplication overflow"); if raw > *remaining { let charged = if *remaining == self.minimum_commission { raw } else { raw.checked_sub(*remaining) .expect("fixed remaining commission underflow") }; *remaining = FixedMoney::ZERO; charged } else { let charged = if *remaining == self.minimum_commission { self.minimum_commission } else { FixedMoney::ZERO }; *remaining = remaining .checked_sub(raw) .expect("fixed remaining commission underflow"); charged } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FixedLot { pub acquired_date: NaiveDate, pub quantity: u64, pub entry_price: FixedMoney, } #[derive(Debug, Clone, Default)] pub struct FixedLotBook { lots: VecDeque, pub realized_pnl: FixedMoney, pub quantity: u64, } impl FixedLotBook { pub fn buy(&mut self, date: NaiveDate, quantity: u64, price: FixedMoney) { if quantity == 0 { return; } self.lots.push_back(FixedLot { acquired_date: date, quantity, entry_price: price, }); self.quantity = self.quantity.saturating_add(quantity); } pub fn sell(&mut self, quantity: u64, price: FixedMoney) -> Result { if quantity > self.quantity { return Err(format!( "fixed sell quantity {} exceeds current quantity {}", quantity, self.quantity )); } let mut remaining = quantity; let mut realized = FixedMoney::ZERO; while remaining > 0 { let Some(mut lot) = self.lots.pop_front() else { return Err("fixed lot book is empty while selling".to_string()); }; let sold = remaining.min(lot.quantity); let price_delta = price .checked_sub(lot.entry_price) .and_then(|delta| delta.checked_mul_quantity(sold)) .ok_or_else(|| "fixed realized PnL overflow".to_string())?; realized = realized .checked_add(price_delta) .ok_or_else(|| "fixed realized PnL overflow".to_string())?; lot.quantity -= sold; remaining -= sold; if lot.quantity > 0 { self.lots.push_front(lot); } } self.quantity -= quantity; self.realized_pnl = self .realized_pnl .checked_add(realized) .ok_or_else(|| "fixed realized PnL overflow".to_string())?; Ok(realized) } pub fn market_value(&self, mark_price: FixedMoney) -> FixedMoney { mark_price .checked_mul_quantity(self.quantity) .expect("fixed market value overflow") } pub fn unrealized_pnl(&self, mark_price: FixedMoney) -> FixedMoney { self.lots.iter().fold(FixedMoney::ZERO, |total, lot| { let delta = mark_price .checked_sub(lot.entry_price) .and_then(|value| value.checked_mul_quantity(lot.quantity)) .expect("fixed unrealized PnL overflow"); total .checked_add(delta) .expect("fixed unrealized PnL overflow") }) } } #[derive(Debug, Clone)] pub struct FixedAccount { pub cash: FixedMoney, pub units: FixedMoney, pub external_cash_flow_total: FixedMoney, } impl FixedAccount { pub fn new(initial_cash: FixedMoney) -> Self { Self { cash: initial_cash, units: initial_cash, external_cash_flow_total: FixedMoney::ZERO, } } pub fn apply_external_cash_flow( &mut self, amount: FixedMoney, unit_nav: FixedMoney, ) -> Result<(), String> { if unit_nav.raw() <= 0 { return Err("fixed unit NAV must be positive".to_string()); } let exact_units_raw = amount .raw() .checked_mul(MONEY_SCALE) .and_then(|value| value.checked_div(unit_nav.raw())) .ok_or_else(|| "fixed external flow unit conversion overflow".to_string())?; self.cash = self .cash .checked_add(amount) .ok_or_else(|| "fixed cash overflow".to_string())?; self.units = self .units .checked_add(FixedMoney::from_raw(exact_units_raw)) .ok_or_else(|| "fixed units overflow".to_string())?; self.external_cash_flow_total = self .external_cash_flow_total .checked_add(amount) .ok_or_else(|| "fixed external flow overflow".to_string())?; Ok(()) } pub fn unit_nav(&self, total_equity: FixedMoney) -> Result { if self.units.raw() <= 0 { return Err("fixed account has no units".to_string()); } let raw = total_equity .raw() .checked_mul(MONEY_SCALE) .and_then(|value| value.checked_div(self.units.raw())) .ok_or_else(|| "fixed unit NAV overflow".to_string())?; Ok(FixedMoney::from_raw(raw)) } } #[cfg(test)] mod tests { use super::*; use crate::cost::{ChinaAShareCostModel, CostModel}; use crate::risk_control::TradingConstraintConfig; fn fixed_model() -> FixedChinaAShareCostModel { let config = TradingConstraintConfig::default(); FixedChinaAShareCostModel { commission_rate: FixedMoney::from_f64(config.commission_rate).unwrap(), stamp_tax_rate_before_change: FixedMoney::from_f64(config.stamp_tax_rate_before_change) .unwrap(), stamp_tax_rate_after_change: FixedMoney::from_f64(config.stamp_tax_rate_after_change) .unwrap(), stamp_tax_change_date: config.stamp_tax_change_date, minimum_commission: FixedMoney::from_f64(config.minimum_commission).unwrap(), transfer_fee_rate: FixedMoney::from_f64(config.transfer_fee_rate).unwrap(), } } #[test] fn decimal_parser_rounds_only_beyond_money_scale() { assert_eq!( FixedMoney::from_decimal_str("1.234567").unwrap().raw(), 1_234_567 ); assert_eq!( FixedMoney::from_decimal_str("1.2345675").unwrap().raw(), 1_234_568 ); assert_eq!( FixedMoney::from_decimal_str("-0.0000014").unwrap().raw(), -1 ); } #[test] fn runtime_cost_model_matches_fixed_execution_primitive() { let fixed = fixed_model(); let float = ChinaAShareCostModel::default(); let dates = [ NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(), NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(), ]; for gross in [0.01, 10.0, 16_666.67, 248_059.812, 1_000_000.01] { let fixed_gross = FixedMoney::from_f64(gross).unwrap(); for date in dates { for side in [OrderSide::Buy, OrderSide::Sell] { let expected = float.calculate(date, side, gross); let actual = fixed.calculate(date, side, fixed_gross); for (actual, expected) in [ (actual.commission, expected.commission), (actual.stamp_tax, expected.stamp_tax), (actual.transfer_fee, expected.transfer_fee), ] { assert_eq!(actual.to_f64(), expected); } } } } } #[test] fn runtime_split_commission_matches_fixed_execution_primitive() { let fixed = fixed_model(); let float = ChinaAShareCostModel::default(); let mut fixed_state = BTreeMap::new(); let mut float_state = BTreeMap::new(); let mut fixed_total = FixedMoney::ZERO; let mut float_total = 0.0; for gross in [1000.0, 2000.0, 4000.0, 40_000.0] { let fixed_fee = fixed.commission_for_order_fill( FixedMoney::from_f64(gross).unwrap(), Some(42), &mut fixed_state, ); let float_fee = float.commission_for_order_fill(gross, Some(42), &mut float_state); fixed_total = fixed_total.checked_add(fixed_fee).unwrap(); float_total += float_fee; } assert_eq!(fixed_total.to_f64(), float_total); } #[test] fn fixed_budget_never_exceeds_cash_after_cost() { let model = fixed_model(); let date = NaiveDate::from_ymd_opt(2025, 2, 3).unwrap(); let cash = FixedMoney::from_decimal_str("99880.00").unwrap(); let price = FixedMoney::from_decimal_str("19.9731").unwrap(); let mut quantity = 5_000u64; while quantity > 0 { let gross = price.checked_mul_quantity(quantity).unwrap(); if gross .checked_add(model.calculate(date, OrderSide::Buy, gross).total()) .unwrap() <= cash { break; } quantity -= 100; } let gross = price.checked_mul_quantity(quantity).unwrap(); let total = gross .checked_add(model.calculate(date, OrderSide::Buy, gross).total()) .unwrap(); assert!(total <= cash); assert!(quantity < 5_000); } #[test] fn fixed_budget_comparison_rejects_one_micro_yuan_overrun() { assert_eq!(FixedMoney::f64_fits_within(100.0, 100.0), Some(true)); assert_eq!(FixedMoney::f64_fits_within(100.000001, 100.0), Some(false)); assert_eq!( FixedMoney::f64_fits_within(100.000001, f64::INFINITY), Some(true) ); } #[test] fn fixed_fifo_pnl_and_external_flow_are_deterministic() { let day_one = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(); let day_two = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(); let mut book = FixedLotBook::default(); book.buy(day_one, 100, FixedMoney::from_decimal_str("10.01").unwrap()); book.buy(day_two, 100, FixedMoney::from_decimal_str("10.03").unwrap()); let realized = book .sell(150, FixedMoney::from_decimal_str("10.11").unwrap()) .unwrap(); assert_eq!(realized.raw(), 14_000_000); assert_eq!(book.quantity, 50); assert_eq!( book.unrealized_pnl(FixedMoney::from_decimal_str("10.20").unwrap()) .raw(), 8_500_000 ); let mut account = FixedAccount::new(FixedMoney::from_decimal_str("100.00").unwrap()); account .apply_external_cash_flow( FixedMoney::from_decimal_str("50.00").unwrap(), FixedMoney::from_decimal_str("1.00").unwrap(), ) .unwrap(); assert_eq!(account.units.raw(), 150 * MONEY_SCALE); assert_eq!( account .unit_nav(FixedMoney::from_decimal_str("150.00").unwrap()) .unwrap() .raw(), MONEY_SCALE ); assert_eq!(account.external_cash_flow_total.raw(), 50 * MONEY_SCALE); } }