Align China A-share costs with rqalpha rules
This commit is contained in:
@@ -463,6 +463,7 @@ where
|
||||
let provisional_target_qty = desired_qty.clamp(min_target_qty, max_target_qty);
|
||||
if current_qty > provisional_target_qty {
|
||||
projected_cash += self.estimated_sell_net_cash(
|
||||
date,
|
||||
price,
|
||||
current_qty.saturating_sub(provisional_target_qty),
|
||||
);
|
||||
@@ -484,6 +485,7 @@ where
|
||||
if target_qty > constraint.current_qty {
|
||||
let desired_additional = target_qty - constraint.current_qty;
|
||||
let affordable_additional = self.affordable_buy_quantity(
|
||||
date,
|
||||
projected_cash,
|
||||
None,
|
||||
constraint.price,
|
||||
@@ -494,6 +496,7 @@ where
|
||||
.clamp(constraint.min_target_qty, constraint.max_target_qty);
|
||||
if target_qty > constraint.current_qty {
|
||||
projected_cash -= self.estimated_buy_cash_out(
|
||||
date,
|
||||
constraint.price,
|
||||
target_qty - constraint.current_qty,
|
||||
);
|
||||
@@ -582,21 +585,21 @@ where
|
||||
current_qty.saturating_add(additional_limit)
|
||||
}
|
||||
|
||||
fn estimated_sell_net_cash(&self, price: f64, quantity: u32) -> f64 {
|
||||
fn estimated_sell_net_cash(&self, date: NaiveDate, price: f64, quantity: u32) -> f64 {
|
||||
if quantity == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let gross = price * quantity as f64;
|
||||
let cost = self.cost_model.calculate(OrderSide::Sell, gross);
|
||||
let cost = self.cost_model.calculate(date, OrderSide::Sell, gross);
|
||||
gross - cost.total()
|
||||
}
|
||||
|
||||
fn estimated_buy_cash_out(&self, price: f64, quantity: u32) -> f64 {
|
||||
fn estimated_buy_cash_out(&self, date: NaiveDate, price: f64, quantity: u32) -> f64 {
|
||||
if quantity == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let gross = price * quantity as f64;
|
||||
let cost = self.cost_model.calculate(OrderSide::Buy, gross);
|
||||
let cost = self.cost_model.calculate(date, OrderSide::Buy, gross);
|
||||
gross + cost.total()
|
||||
}
|
||||
|
||||
@@ -705,7 +708,9 @@ where
|
||||
(filled_qty, self.sell_price(snapshot))
|
||||
};
|
||||
let gross_amount = execution_price * filled_qty as f64;
|
||||
let cost = self.cost_model.calculate(OrderSide::Sell, gross_amount);
|
||||
let cost = self
|
||||
.cost_model
|
||||
.calculate(date, OrderSide::Sell, gross_amount);
|
||||
let net_cash = gross_amount - cost.total();
|
||||
|
||||
let realized_pnl = portfolio
|
||||
@@ -950,6 +955,7 @@ where
|
||||
.map(|start_time| date.and_time(start_time));
|
||||
let quotes = data.execution_quotes_on(date, symbol);
|
||||
if let Some(estimated) = self.select_buy_sizing_fill(
|
||||
date,
|
||||
snapshot,
|
||||
quotes,
|
||||
start_cursor,
|
||||
@@ -963,6 +969,7 @@ where
|
||||
|
||||
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
|
||||
let fallback_qty = self.affordable_buy_quantity(
|
||||
date,
|
||||
portfolio.cash(),
|
||||
Some(value_budget),
|
||||
execution_price,
|
||||
@@ -994,6 +1001,7 @@ where
|
||||
|
||||
fn select_buy_sizing_fill(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
quotes: &[IntradayExecutionQuote],
|
||||
start_cursor: Option<NaiveDateTime>,
|
||||
@@ -1057,7 +1065,9 @@ where
|
||||
take_qty = take_qty.saturating_sub(lot);
|
||||
continue;
|
||||
}
|
||||
let candidate_cost = self.cost_model.calculate(OrderSide::Buy, candidate_gross);
|
||||
let candidate_cost =
|
||||
self.cost_model
|
||||
.calculate(date, OrderSide::Buy, candidate_gross);
|
||||
if candidate_gross + candidate_cost.total() <= cash + 1e-6 {
|
||||
break;
|
||||
}
|
||||
@@ -1166,6 +1176,7 @@ where
|
||||
} else {
|
||||
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
|
||||
let filled_qty = self.affordable_buy_quantity(
|
||||
date,
|
||||
portfolio.cash(),
|
||||
value_budget.map(|budget| budget + 400.0),
|
||||
execution_price,
|
||||
@@ -1189,7 +1200,9 @@ where
|
||||
|
||||
let cash_before = portfolio.cash();
|
||||
let gross_amount = execution_price * filled_qty as f64;
|
||||
let cost = self.cost_model.calculate(OrderSide::Buy, gross_amount);
|
||||
let cost = self
|
||||
.cost_model
|
||||
.calculate(date, OrderSide::Buy, gross_amount);
|
||||
let cash_out = gross_amount + cost.total();
|
||||
|
||||
portfolio.apply_cash_delta(-cash_out);
|
||||
@@ -1290,6 +1303,7 @@ where
|
||||
|
||||
fn affordable_buy_quantity(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
cash: f64,
|
||||
gross_limit: Option<f64>,
|
||||
price: f64,
|
||||
@@ -1304,7 +1318,7 @@ where
|
||||
quantity = quantity.saturating_sub(lot);
|
||||
continue;
|
||||
}
|
||||
let cost = self.cost_model.calculate(OrderSide::Buy, gross);
|
||||
let cost = self.cost_model.calculate(date, OrderSide::Buy, gross);
|
||||
if gross + cost.total() <= cash + 1e-6 {
|
||||
return quantity;
|
||||
}
|
||||
@@ -1382,6 +1396,7 @@ where
|
||||
let execution_price = self.snapshot_execution_price(snapshot, side);
|
||||
let quantity = match side {
|
||||
OrderSide::Buy => self.affordable_buy_quantity(
|
||||
date,
|
||||
cash_limit.unwrap_or(f64::INFINITY),
|
||||
gross_limit,
|
||||
execution_price,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::events::OrderSide;
|
||||
|
||||
pub const STOCK_PIT_TAX_CHANGE_DATE: (i32, u32, u32) = (2023, 8, 28);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TradingCost {
|
||||
pub commission: f64,
|
||||
@@ -13,13 +17,14 @@ impl TradingCost {
|
||||
}
|
||||
|
||||
pub trait CostModel {
|
||||
fn calculate(&self, side: OrderSide, gross_amount: f64) -> TradingCost;
|
||||
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ChinaAShareCostModel {
|
||||
pub commission_rate: f64,
|
||||
pub stamp_tax_rate: f64,
|
||||
pub stamp_tax_rate_before_change: f64,
|
||||
pub stamp_tax_rate_after_change: f64,
|
||||
pub minimum_commission: f64,
|
||||
}
|
||||
|
||||
@@ -27,14 +32,45 @@ impl Default for ChinaAShareCostModel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
commission_rate: 0.0003,
|
||||
stamp_tax_rate: 0.001,
|
||||
stamp_tax_rate_before_change: 0.001,
|
||||
stamp_tax_rate_after_change: 0.0005,
|
||||
minimum_commission: 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChinaAShareCostModel {
|
||||
pub fn commission_for(&self, gross_amount: f64) -> f64 {
|
||||
if gross_amount <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
(gross_amount * self.commission_rate).max(self.minimum_commission)
|
||||
}
|
||||
|
||||
pub fn stamp_tax_rate_for(&self, date: NaiveDate) -> f64 {
|
||||
let change_date = NaiveDate::from_ymd_opt(
|
||||
STOCK_PIT_TAX_CHANGE_DATE.0,
|
||||
STOCK_PIT_TAX_CHANGE_DATE.1,
|
||||
STOCK_PIT_TAX_CHANGE_DATE.2,
|
||||
)
|
||||
.expect("valid pit tax change date");
|
||||
if date < change_date {
|
||||
self.stamp_tax_rate_before_change
|
||||
} else {
|
||||
self.stamp_tax_rate_after_change
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
gross_amount * self.stamp_tax_rate_for(date)
|
||||
}
|
||||
}
|
||||
|
||||
impl CostModel for ChinaAShareCostModel {
|
||||
fn calculate(&self, side: OrderSide, gross_amount: f64) -> TradingCost {
|
||||
fn calculate(&self, date: NaiveDate, side: OrderSide, gross_amount: f64) -> TradingCost {
|
||||
if gross_amount <= 0.0 {
|
||||
return TradingCost {
|
||||
commission: 0.0,
|
||||
@@ -42,11 +78,8 @@ impl CostModel for ChinaAShareCostModel {
|
||||
};
|
||||
}
|
||||
|
||||
let commission = (gross_amount * self.commission_rate).max(self.minimum_commission);
|
||||
let stamp_tax = match side {
|
||||
OrderSide::Buy => 0.0,
|
||||
OrderSide::Sell => gross_amount * self.stamp_tax_rate,
|
||||
};
|
||||
let commission = self.commission_for(gross_amount);
|
||||
let stamp_tax = self.stamp_tax_for(date, side, gross_amount);
|
||||
|
||||
TradingCost {
|
||||
commission,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use rhai::{Dynamic, Engine, Map, Scope};
|
||||
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField};
|
||||
use crate::engine::BacktestError;
|
||||
use crate::events::OrderSide;
|
||||
@@ -346,11 +347,12 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
|
||||
fn buy_commission(&self, gross_amount: f64) -> f64 {
|
||||
(gross_amount * 0.0003).max(5.0)
|
||||
ChinaAShareCostModel::default().commission_for(gross_amount)
|
||||
}
|
||||
|
||||
fn sell_cost(&self, gross_amount: f64) -> f64 {
|
||||
(gross_amount * 0.0003).max(5.0) + (gross_amount * 0.001)
|
||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
model.commission_for(gross_amount) + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||
}
|
||||
|
||||
fn round_lot_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
|
||||
@@ -542,7 +544,7 @@ impl PlatformExprStrategy {
|
||||
+ Duration::seconds(1),
|
||||
});
|
||||
let gross_amount = fill.price * fill.quantity as f64;
|
||||
let net_cash = gross_amount - self.sell_cost(gross_amount);
|
||||
let net_cash = gross_amount - self.sell_cost(date, gross_amount);
|
||||
projected
|
||||
.position_mut(symbol)
|
||||
.sell(fill.quantity, fill.price)
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::sync::OnceLock;
|
||||
|
||||
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
||||
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::data::{DataSet, PriceField};
|
||||
use crate::engine::BacktestError;
|
||||
use crate::events::OrderSide;
|
||||
@@ -576,11 +577,12 @@ impl JqMicroCapStrategy {
|
||||
}
|
||||
|
||||
fn buy_commission(&self, gross_amount: f64) -> f64 {
|
||||
(gross_amount * 0.0003).max(5.0)
|
||||
ChinaAShareCostModel::default().commission_for(gross_amount)
|
||||
}
|
||||
|
||||
fn sell_cost(&self, gross_amount: f64) -> f64 {
|
||||
(gross_amount * 0.0003).max(5.0) + (gross_amount * 0.001)
|
||||
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
|
||||
let model = ChinaAShareCostModel::default();
|
||||
model.commission_for(gross_amount) + model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
|
||||
}
|
||||
|
||||
fn round_lot_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
|
||||
@@ -750,7 +752,7 @@ impl JqMicroCapStrategy {
|
||||
+ Duration::seconds(1),
|
||||
});
|
||||
let gross_amount = fill.price * fill.quantity as f64;
|
||||
let net_cash = gross_amount - self.sell_cost(gross_amount);
|
||||
let net_cash = gross_amount - self.sell_cost(date, gross_amount);
|
||||
projected
|
||||
.position_mut(symbol)
|
||||
.sell(fill.quantity, fill.price)
|
||||
|
||||
Reference in New Issue
Block a user