use std::collections::BTreeMap; use chrono::NaiveDate; use crate::events::OrderSide; use crate::fixed_point::{FixedChinaAShareCostModel, FixedMoney, FixedTradingCost}; use crate::risk_control::TradingConstraintConfig; #[derive(Debug, Clone, Copy)] pub struct TradingCost { pub commission: f64, pub stamp_tax: f64, pub transfer_fee: f64, } impl TradingCost { pub fn total(self) -> f64 { self.fixed_total().to_f64() } pub fn fixed_total(self) -> FixedMoney { FixedMoney::checked_sum_f64([self.commission, self.stamp_tax, self.transfer_fee]) .expect("trading costs must be finite fixed-point money") } fn from_fixed(value: FixedTradingCost) -> Self { Self { commission: value.commission.to_f64(), stamp_tax: value.stamp_tax.to_f64(), transfer_fee: value.transfer_fee.to_f64(), } } } pub trait CostModel { fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost; fn calculate_with_order_state( &self, date: NaiveDate, side: OrderSide, gross_amount: f64, _order_id: Option, _commission_state: &mut BTreeMap, ) -> TradingCost { self.calculate(date, side, gross_amount) } } #[derive(Debug, Clone, Copy)] pub struct ChinaAShareCostModel { fixed: FixedChinaAShareCostModel, } impl Default for ChinaAShareCostModel { fn default() -> Self { Self::from_trading_constraints(TradingConstraintConfig::default()) } } impl ChinaAShareCostModel { pub fn from_trading_constraints(config: TradingConstraintConfig) -> Self { Self { fixed: FixedChinaAShareCostModel { commission_rate: Self::fixed_money(config.commission_rate, "commission rate"), stamp_tax_rate_before_change: Self::fixed_money( config.stamp_tax_rate_before_change, "stamp tax rate before change", ), stamp_tax_rate_after_change: Self::fixed_money( config.stamp_tax_rate_after_change, "stamp tax rate after change", ), stamp_tax_change_date: config.stamp_tax_change_date, minimum_commission: Self::fixed_money( config.minimum_commission, "minimum commission", ), transfer_fee_rate: Self::fixed_money(config.transfer_fee_rate, "transfer fee rate"), }, } } pub fn set_commission_rate(&mut self, value: f64) { self.fixed.commission_rate = Self::fixed_money(value, "commission rate"); } pub fn set_minimum_commission(&mut self, value: f64) { self.fixed.minimum_commission = Self::fixed_money(value, "minimum commission"); } pub fn set_transfer_fee_rate(&mut self, value: f64) { self.fixed.transfer_fee_rate = Self::fixed_money(value, "transfer fee rate"); } pub fn set_stamp_tax_rate_before_change(&mut self, value: f64) { self.fixed.stamp_tax_rate_before_change = Self::fixed_money(value, "stamp tax rate before change"); } pub fn set_stamp_tax_rate_after_change(&mut self, value: f64) { self.fixed.stamp_tax_rate_after_change = Self::fixed_money(value, "stamp tax rate after change"); } pub fn set_stamp_tax_change_date(&mut self, value: NaiveDate) { self.fixed.stamp_tax_change_date = value; } pub fn commission_rate(&self) -> f64 { self.fixed.commission_rate.to_f64() } pub fn minimum_commission(&self) -> f64 { self.fixed.minimum_commission.to_f64() } pub fn transfer_fee_rate(&self) -> f64 { self.fixed.transfer_fee_rate.to_f64() } pub fn stamp_tax_rate_before_change(&self) -> f64 { self.fixed.stamp_tax_rate_before_change.to_f64() } pub fn stamp_tax_rate_after_change(&self) -> f64 { self.fixed.stamp_tax_rate_after_change.to_f64() } pub fn stamp_tax_change_date(&self) -> NaiveDate { self.fixed.stamp_tax_change_date } pub fn with_commission_rate(mut self, value: f64) -> Self { self.set_commission_rate(value); self } pub fn with_minimum_commission(mut self, value: f64) -> Self { self.set_minimum_commission(value); self } pub fn with_stamp_tax_rates(mut self, before: f64, after: f64) -> Self { self.set_stamp_tax_rate_before_change(before); self.set_stamp_tax_rate_after_change(after); self } pub fn commission_for(&self, gross_amount: f64) -> f64 { if gross_amount <= 0.0 { return 0.0; } self.fixed_model() .commission_for(Self::fixed_money(gross_amount, "gross amount")) .to_f64() } pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 { self.fixed.stamp_tax_rate_for(date).to_f64() } pub fn stamp_tax_for(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> f64 { if gross_amount <= 0.0 || side == OrderSide::Buy { return 0.0; } self.fixed_model() .stamp_tax_for(date, side, Self::fixed_money(gross_amount, "gross amount")) .to_f64() } pub fn transfer_fee_for(&self, gross_amount: f64) -> f64 { if gross_amount <= 0.0 { return 0.0; } self.fixed_model() .transfer_fee_for(Self::fixed_money(gross_amount, "gross amount")) .to_f64() } pub fn commission_for_order_fill( &self, gross_amount: f64, order_id: Option, commission_state: &mut BTreeMap, ) -> f64 { if gross_amount <= 0.0 { return 0.0; } let Some(order_id) = order_id else { return self.commission_for(gross_amount); }; let remaining_minimum = commission_state .entry(order_id) .or_insert(self.fixed.minimum_commission.to_f64()); let mut fixed_remaining = Self::fixed_money(*remaining_minimum, "remaining commission"); let charged = self.fixed_model().commission_for_order_fill_remaining( Self::fixed_money(gross_amount, "gross amount"), &mut fixed_remaining, ); *remaining_minimum = fixed_remaining.to_f64(); charged.to_f64() } fn fixed_money(value: f64, label: &str) -> FixedMoney { FixedMoney::from_f64(value) .unwrap_or_else(|| panic!("{label} is not representable as fixed-point money: {value}")) } fn fixed_model(&self) -> FixedChinaAShareCostModel { self.fixed } } impl CostModel for ChinaAShareCostModel { fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost { if gross_amount <= 0.0 { return TradingCost { commission: 0.0, stamp_tax: 0.0, transfer_fee: 0.0, }; } TradingCost::from_fixed(self.fixed_model().calculate( date, side, Self::fixed_money(gross_amount, "gross amount"), )) } fn calculate_with_order_state( &self, date: NaiveDate, side: OrderSide, gross_amount: f64, order_id: Option, commission_state: &mut BTreeMap, ) -> TradingCost { if gross_amount <= 0.0 { return TradingCost { commission: 0.0, stamp_tax: 0.0, transfer_fee: 0.0, }; } let fixed_model = self.fixed_model(); let fixed_gross = Self::fixed_money(gross_amount, "gross amount"); let commission = if let Some(order_id) = order_id { let remaining = commission_state .entry(order_id) .or_insert(self.fixed.minimum_commission.to_f64()); let mut fixed_remaining = Self::fixed_money(*remaining, "remaining commission"); let commission = fixed_model.commission_for_order_fill_remaining(fixed_gross, &mut fixed_remaining); *remaining = fixed_remaining.to_f64(); commission } else { fixed_model.commission_for(fixed_gross) }; TradingCost::from_fixed(FixedTradingCost { commission, stamp_tax: fixed_model.stamp_tax_for(date, side, fixed_gross), transfer_fee: fixed_model.transfer_fee_for(fixed_gross), }) } } #[cfg(test)] mod tests { use super::*; #[test] fn default_quantizes_fees_to_micro_yuan() { let model = ChinaAShareCostModel::default(); let date = NaiveDate::from_ymd_opt(2025, 11, 11).expect("valid date"); assert!((model.commission_for(248_059.812) - 74.417944).abs() < 1e-12); assert!( (model.stamp_tax_for(date, OrderSide::Sell, 245_747.007) - 122.873504).abs() < 1e-12 ); } #[test] fn cost_model_can_use_configurable_stamp_tax_change_date() { let config = TradingConstraintConfig { commission_rate: 0.0003, minimum_commission: 5.0, transfer_fee_rate: 0.00001, stamp_tax_rate_before_change: 0.002, stamp_tax_rate_after_change: 0.001, stamp_tax_change_date: NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"), ..TradingConstraintConfig::default() }; let model = ChinaAShareCostModel::from_trading_constraints(config); assert!((model.transfer_fee_for(10_000.0) - 0.1).abs() < 1e-12); assert!( (model.stamp_tax_for( NaiveDate::from_ymd_opt(2025, 1, 9).expect("valid date"), OrderSide::Sell, 10_000.0 ) - 20.0) .abs() < 1e-9 ); assert!( (model.stamp_tax_for( NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"), OrderSide::Sell, 10_000.0 ) - 10.0) .abs() < 1e-9 ); } }