diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 9b7aaa0..9ec0e9b 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -3749,6 +3749,7 @@ where gross_amount, commission: cost.commission, stamp_tax: cost.stamp_tax, + transfer_fee: cost.transfer_fee, net_cash_flow: net_cash, reason: reason.to_string(), }); @@ -5400,6 +5401,7 @@ where gross_amount, commission: cost.commission, stamp_tax: cost.stamp_tax, + transfer_fee: cost.transfer_fee, net_cash_flow: -cash_out, reason: reason.to_string(), }); diff --git a/crates/fidc-core/src/cost.rs b/crates/fidc-core/src/cost.rs index 4aff6ef..7537692 100644 --- a/crates/fidc-core/src/cost.rs +++ b/crates/fidc-core/src/cost.rs @@ -9,11 +9,12 @@ use crate::risk_control::TradingConstraintConfig; pub struct TradingCost { pub commission: f64, pub stamp_tax: f64, + pub transfer_fee: f64, } impl TradingCost { pub fn total(self) -> f64 { - self.commission + self.stamp_tax + self.commission + self.stamp_tax + self.transfer_fee } } @@ -39,6 +40,7 @@ pub struct ChinaAShareCostModel { pub stamp_tax_rate_after_change: f64, pub stamp_tax_change_date: NaiveDate, pub minimum_commission: f64, + pub transfer_fee_rate: f64, } impl Default for ChinaAShareCostModel { @@ -55,6 +57,7 @@ impl ChinaAShareCostModel { stamp_tax_rate_after_change: config.stamp_tax_rate_after_change, stamp_tax_change_date: config.stamp_tax_change_date, minimum_commission: config.minimum_commission, + transfer_fee_rate: config.transfer_fee_rate, } } @@ -80,6 +83,13 @@ impl ChinaAShareCostModel { gross_amount * self.stamp_tax_rate_for(date) } + pub fn transfer_fee_for(&self, gross_amount: f64) -> f64 { + if gross_amount <= 0.0 { + return 0.0; + } + gross_amount * self.transfer_fee_rate + } + pub fn commission_for_order_fill( &self, gross_amount: f64, @@ -124,15 +134,18 @@ impl CostModel for ChinaAShareCostModel { return TradingCost { commission: 0.0, stamp_tax: 0.0, + transfer_fee: 0.0, }; } let commission = self.commission_for(gross_amount); let stamp_tax = self.stamp_tax_for(date, side, gross_amount); + let transfer_fee = self.transfer_fee_for(gross_amount); TradingCost { commission, stamp_tax, + transfer_fee, } } @@ -148,15 +161,18 @@ impl CostModel for ChinaAShareCostModel { return TradingCost { commission: 0.0, stamp_tax: 0.0, + transfer_fee: 0.0, }; } let commission = self.commission_for_order_fill(gross_amount, order_id, commission_state); let stamp_tax = self.stamp_tax_for(date, side, gross_amount); + let transfer_fee = self.transfer_fee_for(gross_amount); TradingCost { commission, stamp_tax, + transfer_fee, } } } @@ -181,6 +197,7 @@ mod tests { 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"), @@ -188,6 +205,8 @@ mod tests { }; 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"), diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index e9d9b23..c0331f9 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -196,7 +196,7 @@ impl BacktestResult { quantity: fill.quantity, price: fill.price, gross_amount: fill.gross_amount, - transaction_cost: fill.commission + fill.stamp_tax, + transaction_cost: fill.commission + fill.stamp_tax + fill.transfer_fee, net_cash_flow: fill.net_cash_flow, reason: fill.reason.clone(), }) @@ -3157,6 +3157,7 @@ where gross_amount: reinvest_cash, commission: 0.0, stamp_tax: 0.0, + transfer_fee: 0.0, net_cash_flow: -reinvest_cash, reason: "dividend_reinvestment".to_string(), }); diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index 554fc18..6aafe4a 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -125,6 +125,7 @@ pub struct FillEvent { pub gross_amount: f64, pub commission: f64, pub stamp_tax: f64, + pub transfer_fee: f64, pub net_cash_flow: f64, pub reason: String, } diff --git a/crates/fidc-core/src/futures.rs b/crates/fidc-core/src/futures.rs index be05c07..5a85f68 100644 --- a/crates/fidc-core/src/futures.rs +++ b/crates/fidc-core/src/futures.rs @@ -837,6 +837,7 @@ impl FuturesAccountState { gross_amount: notional, commission: intent.transaction_cost.max(0.0), stamp_tax: 0.0, + transfer_fee: 0.0, net_cash_flow: cash_delta, reason: format!( "{} direction={} effect={}", diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index f144a91..b94f30f 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -90,6 +90,8 @@ pub struct StrategyExecutionSpec { alias = "minCommission" )] pub minimum_commission: Option, + #[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")] + pub transfer_fee_rate: Option, #[serde(default, alias = "stamp_tax_rate")] pub stamp_tax_rate: Option, #[serde(default, alias = "stamp_tax_rate_before_change")] @@ -162,6 +164,8 @@ pub struct StrategyEngineConfig { alias = "minCommission" )] pub minimum_commission: Option, + #[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")] + pub transfer_fee_rate: Option, #[serde(default, alias = "stamp_tax_rate")] pub stamp_tax_rate: Option, #[serde(default, alias = "stamp_tax_rate_before_change")] @@ -282,6 +286,8 @@ pub struct StrategyRiskPolicySpec { alias = "minCommission" )] pub minimum_commission: Option, + #[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")] + pub transfer_fee_rate: Option, #[serde(default, alias = "stamp_tax_rate")] pub stamp_tax_rate: Option, #[serde(default, alias = "stamp_tax_rate_before_change")] @@ -343,6 +349,7 @@ const RISK_POLICY_VALUE_ALIAS_GROUPS: &[(&str, &[&str])] = &[ "minimumCommission", &["minimum_commission", "min_commission", "minCommission"], ), + ("transferFeeRate", &["transfer_fee_rate"]), ("stampTaxRate", &["stamp_tax_rate"]), ( "stampTaxRateBeforeChange", @@ -906,6 +913,7 @@ fn apply_cost_overrides( cfg: &mut PlatformExprStrategyConfig, commission_rate: Option, minimum_commission: Option, + transfer_fee_rate: Option, stamp_tax_rate: Option, stamp_tax_rate_before_change: Option, stamp_tax_rate_after_change: Option, @@ -919,6 +927,9 @@ fn apply_cost_overrides( cfg.minimum_commission = Some(value); cfg.risk_config.trading_constraints.minimum_commission = value; } + if let Some(value) = valid_non_negative(transfer_fee_rate) { + cfg.risk_config.trading_constraints.transfer_fee_rate = value; + } if let Some(value) = valid_non_negative(stamp_tax_rate) { cfg.stamp_tax_rate_before_change = Some(value); cfg.stamp_tax_rate_after_change = Some(value); @@ -1074,6 +1085,7 @@ fn apply_risk_policy_overrides( cfg, policy.commission_rate, policy.minimum_commission, + policy.transfer_fee_rate, policy.stamp_tax_rate, policy.stamp_tax_rate_before_change, policy.stamp_tax_rate_after_change, @@ -1495,6 +1507,7 @@ pub fn platform_expr_config_from_spec( &mut cfg, engine.commission_rate, engine.minimum_commission, + engine.transfer_fee_rate, engine.stamp_tax_rate, engine.stamp_tax_rate_before_change, engine.stamp_tax_rate_after_change, @@ -1955,6 +1968,7 @@ pub fn platform_expr_config_from_spec( &mut cfg, execution.commission_rate, execution.minimum_commission, + execution.transfer_fee_rate, execution.stamp_tax_rate, execution.stamp_tax_rate_before_change, execution.stamp_tax_rate_after_change, diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 99c26d5..801ffb6 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -82,6 +82,7 @@ pub struct TradingConstraintConfig { pub liquidity_limit_enabled: bool, pub commission_rate: f64, pub minimum_commission: f64, + pub transfer_fee_rate: f64, pub stamp_tax_rate_before_change: f64, pub stamp_tax_rate_after_change: f64, pub stamp_tax_change_date: NaiveDate, @@ -95,6 +96,7 @@ impl Default for TradingConstraintConfig { liquidity_limit_enabled: true, commission_rate: 0.0003, minimum_commission: 5.0, + transfer_fee_rate: 0.0, stamp_tax_rate_before_change: 0.001, stamp_tax_rate_after_change: 0.0005, stamp_tax_change_date: NaiveDate::from_ymd_opt(2023, 8, 28) diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 1732f40..0369015 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -315,7 +315,7 @@ impl StrategyContext<'_> { let gross_amount = fills.iter().map(|fill| fill.gross_amount).sum::(); let transaction_cost = fills .iter() - .map(|fill| fill.commission + fill.stamp_tax) + .map(|fill| fill.commission + fill.stamp_tax + fill.transfer_fee) .sum::(); let avg_price = if filled_quantity == 0 { 0.0