feat(stock-pool): unify target execution, durable intent state and ETF rules

This commit is contained in:
boris
2026-09-12 03:55:00 +08:00
parent 29eafc79e2
commit 6ffa0346aa
26 changed files with 7223 additions and 38 deletions
+52
View File
@@ -5,6 +5,7 @@ use chrono::NaiveDate;
use crate::events::OrderSide;
use crate::fixed_point::{FixedChinaAShareCostModel, FixedMoney, FixedTradingCost};
use crate::risk_control::TradingConstraintConfig;
use crate::Instrument;
#[derive(Debug, Clone, Copy)]
pub struct TradingCost {
@@ -35,6 +36,17 @@ impl TradingCost {
pub trait CostModel {
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost;
fn calculate_for_instrument(&self, date: NaiveDate, side: OrderSide, gross_amount: f64, _instrument: Option<&Instrument>) -> TradingCost {
self.calculate(date, side, gross_amount)
}
fn calculate_with_order_state_for_instrument(
&self, date: NaiveDate, side: OrderSide, gross_amount: f64,
order_id: Option<u64>, commission_state: &mut BTreeMap<u64,f64>, _instrument: Option<&Instrument>,
) -> TradingCost {
self.calculate_with_order_state(date, side, gross_amount, order_id, commission_state)
}
fn calculate_with_order_state(
&self,
date: NaiveDate,
@@ -215,6 +227,27 @@ impl ChinaAShareCostModel {
}
impl CostModel for ChinaAShareCostModel {
fn calculate_for_instrument(&self, date: NaiveDate, side: OrderSide, gross_amount: f64, instrument: Option<&Instrument>) -> TradingCost {
let mut cost = self.calculate(date, side, gross_amount);
if instrument.is_some_and(Instrument::is_exchange_traded_fund) {
cost.stamp_tax = 0.0;
cost.transfer_fee = 0.0;
}
cost
}
fn calculate_with_order_state_for_instrument(
&self, date: NaiveDate, side: OrderSide, gross_amount: f64,
order_id: Option<u64>, commission_state: &mut BTreeMap<u64,f64>, instrument: Option<&Instrument>,
) -> TradingCost {
let mut cost = self.calculate_with_order_state(date, side, gross_amount, order_id, commission_state);
if instrument.is_some_and(Instrument::is_exchange_traded_fund) {
cost.stamp_tax = 0.0;
cost.transfer_fee = 0.0;
}
cost
}
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost {
if gross_amount <= 0.0 {
return TradingCost {
@@ -273,6 +306,25 @@ impl CostModel for ChinaAShareCostModel {
mod tests {
use super::*;
#[test]
fn fund_fees_use_admitted_instrument_type_and_share_the_order_commission_budget() {
let day=NaiveDate::from_ymd_opt(2026,9,11).unwrap();
let model=ChinaAShareCostModel::from_trading_constraints(TradingConstraintConfig{commission_rate:0.0003,minimum_commission:5.,transfer_fee_rate:0.00001,..Default::default()});
let mut instrument=Instrument{symbol:"510300.SH".into(),name:"fixture".into(),board:"ETF".into(),round_lot:100,listed_at:Some(day),delisted_at:None,status:"active".into()};
for side in [OrderSide::Buy,OrderSide::Sell] {
let cost=model.calculate_for_instrument(day,side,10_000.,Some(&instrument));
assert_eq!(cost.commission,5.);assert_eq!(cost.stamp_tax,0.);assert_eq!(cost.transfer_fee,0.);
let mut state=BTreeMap::new();
let one=model.calculate_with_order_state_for_instrument(day,side,1_000.,Some(1),&mut state,Some(&instrument));
let two=model.calculate_with_order_state_for_instrument(day,side,9_000.,Some(1),&mut state,Some(&instrument));
assert_eq!(one.total()+two.total(),cost.total());
}
instrument.board="SH".into();
let stock=model.calculate_for_instrument(day,OrderSide::Sell,10_000.,Some(&instrument));
assert_eq!(stock.stamp_tax,5.);assert_eq!(stock.transfer_fee,0.1);
assert_eq!(stock.total(),model.calculate(day,OrderSide::Sell,10_000.).total());
}
#[test]
fn default_quantizes_fees_to_micro_yuan() {
let model = ChinaAShareCostModel::default();