将股票执行资金切换为定点账本
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user