补充可配置过户费并纳入成交成本

This commit is contained in:
boris
2026-08-22 14:50:00 +08:00
parent b8776d7169
commit 375b8b2df1
8 changed files with 43 additions and 3 deletions
+2
View File
@@ -3749,6 +3749,7 @@ where
gross_amount, gross_amount,
commission: cost.commission, commission: cost.commission,
stamp_tax: cost.stamp_tax, stamp_tax: cost.stamp_tax,
transfer_fee: cost.transfer_fee,
net_cash_flow: net_cash, net_cash_flow: net_cash,
reason: reason.to_string(), reason: reason.to_string(),
}); });
@@ -5400,6 +5401,7 @@ where
gross_amount, gross_amount,
commission: cost.commission, commission: cost.commission,
stamp_tax: cost.stamp_tax, stamp_tax: cost.stamp_tax,
transfer_fee: cost.transfer_fee,
net_cash_flow: -cash_out, net_cash_flow: -cash_out,
reason: reason.to_string(), reason: reason.to_string(),
}); });
+20 -1
View File
@@ -9,11 +9,12 @@ use crate::risk_control::TradingConstraintConfig;
pub struct TradingCost { pub struct TradingCost {
pub commission: f64, pub commission: f64,
pub stamp_tax: f64, pub stamp_tax: f64,
pub transfer_fee: f64,
} }
impl TradingCost { impl TradingCost {
pub fn total(self) -> f64 { 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_rate_after_change: f64,
pub stamp_tax_change_date: NaiveDate, pub stamp_tax_change_date: NaiveDate,
pub minimum_commission: f64, pub minimum_commission: f64,
pub transfer_fee_rate: f64,
} }
impl Default for ChinaAShareCostModel { impl Default for ChinaAShareCostModel {
@@ -55,6 +57,7 @@ impl ChinaAShareCostModel {
stamp_tax_rate_after_change: config.stamp_tax_rate_after_change, stamp_tax_rate_after_change: config.stamp_tax_rate_after_change,
stamp_tax_change_date: config.stamp_tax_change_date, stamp_tax_change_date: config.stamp_tax_change_date,
minimum_commission: config.minimum_commission, 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) 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( pub fn commission_for_order_fill(
&self, &self,
gross_amount: f64, gross_amount: f64,
@@ -124,15 +134,18 @@ impl CostModel for ChinaAShareCostModel {
return TradingCost { return TradingCost {
commission: 0.0, commission: 0.0,
stamp_tax: 0.0, stamp_tax: 0.0,
transfer_fee: 0.0,
}; };
} }
let commission = self.commission_for(gross_amount); let commission = self.commission_for(gross_amount);
let stamp_tax = self.stamp_tax_for(date, side, gross_amount); let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
let transfer_fee = self.transfer_fee_for(gross_amount);
TradingCost { TradingCost {
commission, commission,
stamp_tax, stamp_tax,
transfer_fee,
} }
} }
@@ -148,15 +161,18 @@ impl CostModel for ChinaAShareCostModel {
return TradingCost { return TradingCost {
commission: 0.0, commission: 0.0,
stamp_tax: 0.0, stamp_tax: 0.0,
transfer_fee: 0.0,
}; };
} }
let commission = self.commission_for_order_fill(gross_amount, order_id, commission_state); 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 stamp_tax = self.stamp_tax_for(date, side, gross_amount);
let transfer_fee = self.transfer_fee_for(gross_amount);
TradingCost { TradingCost {
commission, commission,
stamp_tax, stamp_tax,
transfer_fee,
} }
} }
} }
@@ -181,6 +197,7 @@ mod tests {
let config = TradingConstraintConfig { let config = TradingConstraintConfig {
commission_rate: 0.0003, commission_rate: 0.0003,
minimum_commission: 5.0, minimum_commission: 5.0,
transfer_fee_rate: 0.00001,
stamp_tax_rate_before_change: 0.002, stamp_tax_rate_before_change: 0.002,
stamp_tax_rate_after_change: 0.001, stamp_tax_rate_after_change: 0.001,
stamp_tax_change_date: NaiveDate::from_ymd_opt(2025, 1, 10).expect("valid date"), 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); let model = ChinaAShareCostModel::from_trading_constraints(config);
assert!((model.transfer_fee_for(10_000.0) - 0.1).abs() < 1e-12);
assert!( assert!(
(model.stamp_tax_for( (model.stamp_tax_for(
NaiveDate::from_ymd_opt(2025, 1, 9).expect("valid date"), NaiveDate::from_ymd_opt(2025, 1, 9).expect("valid date"),
+2 -1
View File
@@ -196,7 +196,7 @@ impl BacktestResult {
quantity: fill.quantity, quantity: fill.quantity,
price: fill.price, price: fill.price,
gross_amount: fill.gross_amount, 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, net_cash_flow: fill.net_cash_flow,
reason: fill.reason.clone(), reason: fill.reason.clone(),
}) })
@@ -3157,6 +3157,7 @@ where
gross_amount: reinvest_cash, gross_amount: reinvest_cash,
commission: 0.0, commission: 0.0,
stamp_tax: 0.0, stamp_tax: 0.0,
transfer_fee: 0.0,
net_cash_flow: -reinvest_cash, net_cash_flow: -reinvest_cash,
reason: "dividend_reinvestment".to_string(), reason: "dividend_reinvestment".to_string(),
}); });
+1
View File
@@ -125,6 +125,7 @@ pub struct FillEvent {
pub gross_amount: f64, pub gross_amount: f64,
pub commission: f64, pub commission: f64,
pub stamp_tax: f64, pub stamp_tax: f64,
pub transfer_fee: f64,
pub net_cash_flow: f64, pub net_cash_flow: f64,
pub reason: String, pub reason: String,
} }
+1
View File
@@ -837,6 +837,7 @@ impl FuturesAccountState {
gross_amount: notional, gross_amount: notional,
commission: intent.transaction_cost.max(0.0), commission: intent.transaction_cost.max(0.0),
stamp_tax: 0.0, stamp_tax: 0.0,
transfer_fee: 0.0,
net_cash_flow: cash_delta, net_cash_flow: cash_delta,
reason: format!( reason: format!(
"{} direction={} effect={}", "{} direction={} effect={}",
@@ -90,6 +90,8 @@ pub struct StrategyExecutionSpec {
alias = "minCommission" alias = "minCommission"
)] )]
pub minimum_commission: Option<f64>, pub minimum_commission: Option<f64>,
#[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")]
pub transfer_fee_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate")] #[serde(default, alias = "stamp_tax_rate")]
pub stamp_tax_rate: Option<f64>, pub stamp_tax_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate_before_change")] #[serde(default, alias = "stamp_tax_rate_before_change")]
@@ -162,6 +164,8 @@ pub struct StrategyEngineConfig {
alias = "minCommission" alias = "minCommission"
)] )]
pub minimum_commission: Option<f64>, pub minimum_commission: Option<f64>,
#[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")]
pub transfer_fee_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate")] #[serde(default, alias = "stamp_tax_rate")]
pub stamp_tax_rate: Option<f64>, pub stamp_tax_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate_before_change")] #[serde(default, alias = "stamp_tax_rate_before_change")]
@@ -282,6 +286,8 @@ pub struct StrategyRiskPolicySpec {
alias = "minCommission" alias = "minCommission"
)] )]
pub minimum_commission: Option<f64>, pub minimum_commission: Option<f64>,
#[serde(default, alias = "transfer_fee_rate", alias = "transferFeeRate")]
pub transfer_fee_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate")] #[serde(default, alias = "stamp_tax_rate")]
pub stamp_tax_rate: Option<f64>, pub stamp_tax_rate: Option<f64>,
#[serde(default, alias = "stamp_tax_rate_before_change")] #[serde(default, alias = "stamp_tax_rate_before_change")]
@@ -343,6 +349,7 @@ const RISK_POLICY_VALUE_ALIAS_GROUPS: &[(&str, &[&str])] = &[
"minimumCommission", "minimumCommission",
&["minimum_commission", "min_commission", "minCommission"], &["minimum_commission", "min_commission", "minCommission"],
), ),
("transferFeeRate", &["transfer_fee_rate"]),
("stampTaxRate", &["stamp_tax_rate"]), ("stampTaxRate", &["stamp_tax_rate"]),
( (
"stampTaxRateBeforeChange", "stampTaxRateBeforeChange",
@@ -906,6 +913,7 @@ fn apply_cost_overrides(
cfg: &mut PlatformExprStrategyConfig, cfg: &mut PlatformExprStrategyConfig,
commission_rate: Option<f64>, commission_rate: Option<f64>,
minimum_commission: Option<f64>, minimum_commission: Option<f64>,
transfer_fee_rate: Option<f64>,
stamp_tax_rate: Option<f64>, stamp_tax_rate: Option<f64>,
stamp_tax_rate_before_change: Option<f64>, stamp_tax_rate_before_change: Option<f64>,
stamp_tax_rate_after_change: Option<f64>, stamp_tax_rate_after_change: Option<f64>,
@@ -919,6 +927,9 @@ fn apply_cost_overrides(
cfg.minimum_commission = Some(value); cfg.minimum_commission = Some(value);
cfg.risk_config.trading_constraints.minimum_commission = 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) { if let Some(value) = valid_non_negative(stamp_tax_rate) {
cfg.stamp_tax_rate_before_change = Some(value); cfg.stamp_tax_rate_before_change = Some(value);
cfg.stamp_tax_rate_after_change = Some(value); cfg.stamp_tax_rate_after_change = Some(value);
@@ -1074,6 +1085,7 @@ fn apply_risk_policy_overrides(
cfg, cfg,
policy.commission_rate, policy.commission_rate,
policy.minimum_commission, policy.minimum_commission,
policy.transfer_fee_rate,
policy.stamp_tax_rate, policy.stamp_tax_rate,
policy.stamp_tax_rate_before_change, policy.stamp_tax_rate_before_change,
policy.stamp_tax_rate_after_change, policy.stamp_tax_rate_after_change,
@@ -1495,6 +1507,7 @@ pub fn platform_expr_config_from_spec(
&mut cfg, &mut cfg,
engine.commission_rate, engine.commission_rate,
engine.minimum_commission, engine.minimum_commission,
engine.transfer_fee_rate,
engine.stamp_tax_rate, engine.stamp_tax_rate,
engine.stamp_tax_rate_before_change, engine.stamp_tax_rate_before_change,
engine.stamp_tax_rate_after_change, engine.stamp_tax_rate_after_change,
@@ -1955,6 +1968,7 @@ pub fn platform_expr_config_from_spec(
&mut cfg, &mut cfg,
execution.commission_rate, execution.commission_rate,
execution.minimum_commission, execution.minimum_commission,
execution.transfer_fee_rate,
execution.stamp_tax_rate, execution.stamp_tax_rate,
execution.stamp_tax_rate_before_change, execution.stamp_tax_rate_before_change,
execution.stamp_tax_rate_after_change, execution.stamp_tax_rate_after_change,
+2
View File
@@ -82,6 +82,7 @@ pub struct TradingConstraintConfig {
pub liquidity_limit_enabled: bool, pub liquidity_limit_enabled: bool,
pub commission_rate: f64, pub commission_rate: f64,
pub minimum_commission: f64, pub minimum_commission: f64,
pub transfer_fee_rate: f64,
pub stamp_tax_rate_before_change: f64, pub stamp_tax_rate_before_change: f64,
pub stamp_tax_rate_after_change: f64, pub stamp_tax_rate_after_change: f64,
pub stamp_tax_change_date: NaiveDate, pub stamp_tax_change_date: NaiveDate,
@@ -95,6 +96,7 @@ impl Default for TradingConstraintConfig {
liquidity_limit_enabled: true, liquidity_limit_enabled: true,
commission_rate: 0.0003, commission_rate: 0.0003,
minimum_commission: 5.0, minimum_commission: 5.0,
transfer_fee_rate: 0.0,
stamp_tax_rate_before_change: 0.001, stamp_tax_rate_before_change: 0.001,
stamp_tax_rate_after_change: 0.0005, stamp_tax_rate_after_change: 0.0005,
stamp_tax_change_date: NaiveDate::from_ymd_opt(2023, 8, 28) stamp_tax_change_date: NaiveDate::from_ymd_opt(2023, 8, 28)
+1 -1
View File
@@ -315,7 +315,7 @@ impl StrategyContext<'_> {
let gross_amount = fills.iter().map(|fill| fill.gross_amount).sum::<f64>(); let gross_amount = fills.iter().map(|fill| fill.gross_amount).sum::<f64>();
let transaction_cost = fills let transaction_cost = fills
.iter() .iter()
.map(|fill| fill.commission + fill.stamp_tax) .map(|fill| fill.commission + fill.stamp_tax + fill.transfer_fee)
.sum::<f64>(); .sum::<f64>();
let avg_price = if filled_quantity == 0 { let avg_price = if filled_quantity == 0 {
0.0 0.0