将股票执行资金切换为定点账本

This commit is contained in:
boris
2026-08-25 14:36:15 +08:00
parent c9ddff46dd
commit 92724c6ab0
9 changed files with 640 additions and 250 deletions
+73 -24
View File
@@ -10,6 +10,7 @@ use crate::data::{
};
use crate::engine::BacktestError;
use crate::events::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEvent};
use crate::fixed_point::FixedMoney;
use crate::futures::{FuturesAccountState, FuturesOrderIntent};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
@@ -1750,14 +1751,47 @@ impl OmniMicroCapStrategy {
ChinaAShareCostModel::from_trading_constraints(self.config.risk_config.trading_constraints)
}
fn buy_commission(&self, gross_amount: f64) -> f64 {
self.cost_model().commission_for(gross_amount)
fn buy_cost(&self, gross_amount: f64) -> f64 {
let model = self.cost_model();
FixedMoney::checked_sum_f64([
model.commission_for(gross_amount),
model.transfer_fee_for(gross_amount),
])
.expect("projected buy costs must be finite fixed-point money")
.to_f64()
}
fn sell_cost(&self, date: NaiveDate, gross_amount: f64) -> f64 {
let model = self.cost_model();
model.commission_for(gross_amount)
+ model.stamp_tax_for(date, OrderSide::Sell, gross_amount)
FixedMoney::checked_sum_f64([
model.commission_for(gross_amount),
model.stamp_tax_for(date, OrderSide::Sell, gross_amount),
model.transfer_fee_for(gross_amount),
])
.expect("projected sell costs must be finite fixed-point money")
.to_f64()
}
fn buy_cash_out(&self, gross_amount: f64) -> f64 {
FixedMoney::checked_sum_f64([gross_amount, self.buy_cost(gross_amount)])
.expect("projected buy cash must be finite fixed-point money")
.to_f64()
}
fn sell_net_cash(&self, date: NaiveDate, gross_amount: f64) -> f64 {
let gross = FixedMoney::from_f64(gross_amount)
.expect("projected sell gross must be finite fixed-point money");
gross
.checked_sub(
FixedMoney::from_f64(self.sell_cost(date, gross.to_f64()))
.expect("projected sell costs must be finite fixed-point money"),
)
.expect("projected sell proceeds underflow")
.to_f64()
}
fn fixed_cash_fits(value: f64, limit: f64) -> bool {
FixedMoney::f64_fits_within(value, limit).unwrap_or(false)
}
fn round_lot_quantity(
@@ -1826,7 +1860,7 @@ impl OmniMicroCapStrategy {
let mut quantity = self.round_lot_quantity((cash / sizing_price).floor() as u32, 100, 100);
while quantity > 0 {
let gross_amount = execution_price * quantity as f64;
if gross_amount + self.buy_commission(gross_amount) <= cash + 1e-6 {
if Self::fixed_cash_fits(self.buy_cash_out(gross_amount), cash) {
return quantity;
}
quantity = self.decrement_order_quantity(quantity, 100, 100);
@@ -1874,8 +1908,10 @@ impl OmniMicroCapStrategy {
);
while snapshot_requested_qty > 0 {
let gross_amount = sizing_price * snapshot_requested_qty as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
snapshot_requested_qty = self.decrement_order_quantity(
@@ -1902,8 +1938,10 @@ impl OmniMicroCapStrategy {
let mut quantity = snapshot_requested_qty;
while quantity > 0 {
let gross_amount = projected_execution_price * quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
quantity =
@@ -1918,8 +1956,10 @@ impl OmniMicroCapStrategy {
.unwrap_or(projected_execution_price);
while quantity > 0 {
let gross_amount = execution_price * quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out <= order_value + 1e-6 && cash_out <= projected.cash() + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if Self::fixed_cash_fits(cash_out, order_value)
&& Self::fixed_cash_fits(cash_out, projected.cash())
{
break;
}
quantity =
@@ -1934,11 +1974,15 @@ impl OmniMicroCapStrategy {
next_cursor: date.and_time(self.intraday_execution_start_time()) + Duration::seconds(1),
};
let gross_amount = fill.price * fill.quantity as f64;
let cash_out = gross_amount + self.buy_commission(gross_amount);
if cash_out > projected.cash() + 1e-6 || cash_out > order_value + 1e-6 {
let cash_out = self.buy_cash_out(gross_amount);
if !Self::fixed_cash_fits(cash_out, projected.cash())
|| !Self::fixed_cash_fits(cash_out, order_value)
{
return 0;
}
projected.apply_cash_delta(-cash_out);
projected
.apply_cash_delta(-cash_out)
.expect("projected buy cash must fit fixed-point ledger");
projected
.position_mut(symbol)
.buy(date, fill.quantity, fill.price);
@@ -1994,12 +2038,14 @@ impl OmniMicroCapStrategy {
+ Duration::seconds(1),
});
let gross_amount = fill.price * fill.quantity as f64;
let net_cash = gross_amount - self.sell_cost(date, gross_amount);
let net_cash = self.sell_net_cash(date, gross_amount);
projected
.position_mut(symbol)
.sell(fill.quantity, fill.price)
.ok()?;
projected.apply_cash_delta(net_cash);
projected
.apply_cash_delta(net_cash)
.expect("projected sell cash must fit fixed-point ledger");
*execution_state
.intraday_turnover
.entry(symbol.to_string())
@@ -2144,7 +2190,9 @@ impl OmniMicroCapStrategy {
);
while take_qty > 0 {
let candidate_gross = execution_price * take_qty as f64;
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
if gross_limit
.is_some_and(|limit| !Self::fixed_cash_fits(candidate_gross, limit))
{
take_qty = self.decrement_order_quantity(
take_qty,
minimum_order_quantity,
@@ -2152,9 +2200,8 @@ impl OmniMicroCapStrategy {
);
continue;
}
let candidate_cash =
candidate_gross + self.buy_commission(candidate_gross);
if candidate_cash <= cash + 1e-6 {
let candidate_cash = self.buy_cash_out(candidate_gross);
if Self::fixed_cash_fits(candidate_cash, cash) {
break;
}
take_qty = self.decrement_order_quantity(
@@ -2254,7 +2301,9 @@ impl OmniMicroCapStrategy {
if let Some(cash) = cash_limit {
while take_qty > 0 {
let candidate_gross = gross_amount + quote_price * take_qty as f64;
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
if gross_limit
.is_some_and(|limit| !Self::fixed_cash_fits(candidate_gross, limit))
{
take_qty = self.decrement_order_quantity(
take_qty,
minimum_order_quantity,
@@ -2262,7 +2311,7 @@ impl OmniMicroCapStrategy {
);
continue;
}
if candidate_gross + self.buy_commission(candidate_gross) <= cash + 1e-6 {
if Self::fixed_cash_fits(self.buy_cash_out(candidate_gross), cash) {
break;
}
take_qty = self.decrement_order_quantity(
@@ -2870,8 +2919,8 @@ mod tests {
.stamp_tax_rate_after_change = 0.0005;
let strategy = OmniMicroCapStrategy::new(cfg);
assert!((strategy.buy_commission(100_000.0) - 30.0).abs() < 1e-9);
assert!((strategy.buy_commission(1_000.0) - 5.0).abs() < 1e-9);
assert!((strategy.buy_cost(100_000.0) - 30.0).abs() < 1e-9);
assert!((strategy.buy_cost(1_000.0) - 5.0).abs() < 1e-9);
assert!(
(strategy.sell_cost(NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(), 100_000.0) - 80.0)
.abs()