将股票执行资金切换为定点账本

This commit is contained in:
boris
2026-08-25 14:36:15 +08:00
parent c9ddff46dd
commit 92724c6ab0
9 changed files with 640 additions and 250 deletions
+51 -16
View File
@@ -1,10 +1,9 @@
//! Independent fixed-point acceptance model for money and fee arithmetic.
//! Fixed-point execution primitives for money and fee arithmetic.
//!
//! The execution kernel still exposes f64 because prices and source rows are
//! represented that way today. This module is deliberately separate: it is a
//! deterministic shadow model used to prove that cash, fees, budget checks,
//! FIFO PnL, and external cash flows do not depend on binary floating-point
//! accumulation.
//! 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};
@@ -105,6 +104,10 @@ impl FixedMoney {
self.0.checked_mul(i128::from(quantity)).map(Self)
}
pub fn checked_neg(self) -> Option<Self> {
self.0.checked_neg().map(Self)
}
pub fn checked_mul_rate(self, rate: Self) -> Option<Self> {
let product = self.0.checked_mul(rate.0)?;
let half = MONEY_SCALE / 2;
@@ -116,6 +119,20 @@ impl FixedMoney {
Some(Self(rounded))
}
pub fn checked_sum_f64(values: impl IntoIterator<Item = f64>) -> Option<Self> {
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<bool> {
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())
}
@@ -217,6 +234,20 @@ impl FixedChinaAShareCostModel {
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
@@ -415,7 +446,7 @@ mod tests {
}
#[test]
fn fixed_cost_matches_float_cost_model_within_one_micro_yuan() {
fn runtime_cost_model_matches_fixed_execution_primitive() {
let fixed = fixed_model();
let float = ChinaAShareCostModel::default();
let dates = [
@@ -433,13 +464,7 @@ mod tests {
(actual.stamp_tax, expected.stamp_tax),
(actual.transfer_fee, expected.transfer_fee),
] {
assert!(
(actual.to_f64() - expected).abs() <= 1.0 / MONEY_SCALE_F64,
"fixed={} float={} gross={} date={date} side={side:?}",
actual.to_f64(),
expected,
gross
);
assert_eq!(actual.to_f64(), expected);
}
}
}
@@ -447,7 +472,7 @@ mod tests {
}
#[test]
fn fixed_order_commission_state_matches_float_order_split() {
fn runtime_split_commission_matches_fixed_execution_primitive() {
let fixed = fixed_model();
let float = ChinaAShareCostModel::default();
let mut fixed_state = BTreeMap::new();
@@ -464,7 +489,7 @@ mod tests {
fixed_total = fixed_total.checked_add(fixed_fee).unwrap();
float_total += float_fee;
}
assert!((fixed_total.to_f64() - float_total).abs() <= 4.0 / MONEY_SCALE_F64);
assert_eq!(fixed_total.to_f64(), float_total);
}
#[test]
@@ -493,6 +518,16 @@ mod tests {
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();