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

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
+98 -36
View File
@@ -10,6 +10,7 @@ use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
use crate::fixed_point::FixedMoney;
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, RiskCheckScope};
@@ -2585,7 +2586,7 @@ where
} else {
0.0
};
if buy_cash_out <= projected_cash + 1e-6 {
if Self::fixed_cash_fits(buy_cash_out, projected_cash) {
if proportion_diff < best_proportion_diff - 1e-12
|| ((proportion_diff - best_proportion_diff).abs() <= 1e-12
&& safety_value > best_safety)
@@ -3150,9 +3151,14 @@ where
if quantity == 0 {
return 0.0;
}
let gross = price * quantity as f64;
let cost = self.cost_model.calculate(date, OrderSide::Sell, gross);
gross - cost.total()
let gross = Self::fixed_gross_amount(price, quantity);
let cost = self
.cost_model
.calculate(date, OrderSide::Sell, gross.to_f64());
gross
.checked_sub(cost.fixed_total())
.expect("fixed-point sell proceeds underflow")
.to_f64()
}
fn sell_target_denial_reason(
@@ -3256,9 +3262,23 @@ where
if quantity == 0 {
return 0.0;
}
let gross = price * quantity as f64;
let cost = self.cost_model.calculate(date, OrderSide::Buy, gross);
gross + cost.total()
let gross = Self::fixed_gross_amount(price, quantity);
let cost = self
.cost_model
.calculate(date, OrderSide::Buy, gross.to_f64());
gross
.checked_add(cost.fixed_total())
.expect("fixed-point buy cash overflow")
.to_f64()
}
fn fixed_gross_amount(price: f64, quantity: u32) -> FixedMoney {
FixedMoney::from_f64(price * quantity as f64)
.expect("execution gross amount must be finite fixed-point money")
}
fn fixed_cash_fits(value: f64, limit: f64) -> bool {
FixedMoney::f64_fits_within(value, limit).unwrap_or(false)
}
fn can_afford_minimum_buy(
@@ -3283,8 +3303,10 @@ where
}
let minimum_execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(minimum_buy_quantity));
self.estimated_buy_cash_out(date, minimum_execution_price, minimum_buy_quantity)
<= portfolio.cash() + 1e-6
Self::fixed_cash_fits(
self.estimated_buy_cash_out(date, minimum_execution_price, minimum_buy_quantity),
portfolio.cash(),
)
}
fn process_sell(
@@ -3722,7 +3744,8 @@ where
}
for leg in &execution_legs {
let leg_cash_before = portfolio.cash();
let gross_amount = leg.price * leg.quantity as f64;
let gross_money = Self::fixed_gross_amount(leg.price, leg.quantity);
let gross_amount = gross_money.to_f64();
let cost = self.cost_model.calculate_with_order_state(
date,
OrderSide::Sell,
@@ -3730,7 +3753,10 @@ where
Some(order_id),
commission_state,
);
let net_cash = gross_amount - cost.total();
let net_cash = gross_money
.checked_sub(cost.fixed_total())
.expect("fixed-point sell proceeds underflow")
.to_f64();
let realized_pnl = portfolio
.position_mut(symbol)
.sell_with_mark_price(leg.quantity, leg.price, leg.mark_price)
@@ -3738,7 +3764,9 @@ where
if let Some(position) = portfolio.position_mut_if_exists(symbol) {
position.record_trade_cost(cost.total());
}
portfolio.apply_cash_delta(net_cash);
portfolio
.apply_cash_delta(net_cash)
.map_err(BacktestError::Execution)?;
report.fill_events.push(FillEvent {
date,
@@ -5371,7 +5399,8 @@ where
}
for leg in &execution_legs {
let leg_cash_before = portfolio.cash();
let gross_amount = leg.price * leg.quantity as f64;
let gross_money = Self::fixed_gross_amount(leg.price, leg.quantity);
let gross_amount = gross_money.to_f64();
let cost = self.cost_model.calculate_with_order_state(
date,
OrderSide::Buy,
@@ -5379,9 +5408,14 @@ where
Some(order_id),
commission_state,
);
let cash_out = gross_amount + cost.total();
let cash_out = gross_money
.checked_add(cost.fixed_total())
.expect("fixed-point buy cash overflow")
.to_f64();
portfolio.apply_cash_delta(-cash_out);
portfolio
.apply_cash_delta(-cash_out)
.map_err(BacktestError::Execution)?;
portfolio.position_mut(symbol).buy_with_mark_price(
date,
leg.quantity,
@@ -5766,7 +5800,10 @@ where
let mut quantity =
self.round_buy_quantity(raw_quantity, minimum_order_quantity, order_step_size);
while quantity >= minimum {
if self.estimated_buy_cash_out(date, price, quantity) <= value_budget + 1e-6 {
if Self::fixed_cash_fits(
self.estimated_buy_cash_out(date, price, quantity),
value_budget,
) {
return quantity;
}
quantity =
@@ -5820,7 +5857,10 @@ where
})
.filter(|price| price.is_finite() && *price > 0.0)
.unwrap_or(fallback_price);
if self.estimated_buy_cash_out(date, execution_price, quantity) <= value_budget + 1e-6 {
if Self::fixed_cash_fits(
self.estimated_buy_cash_out(date, execution_price, quantity),
value_budget,
) {
return quantity;
}
quantity =
@@ -5857,7 +5897,7 @@ where
self.round_buy_quantity(requested_qty, minimum_order_quantity, order_step_size);
while quantity > 0 {
let gross = price * quantity as f64;
if gross_limit.is_some_and(|limit| gross > limit + 1e-6) {
if gross_limit.is_some_and(|limit| !Self::fixed_cash_fits(gross, limit)) {
quantity = self.decrement_order_quantity(
quantity,
minimum_order_quantity,
@@ -5866,7 +5906,10 @@ where
continue;
}
let cost = self.cost_model.calculate(date, OrderSide::Buy, gross);
if gross + cost.total() <= cash + 1e-6 {
let cash_out = FixedMoney::checked_sum_f64([gross, cost.total()])
.expect("buy cash must be finite fixed-point money")
.to_f64();
if Self::fixed_cash_fits(cash_out, cash) {
return quantity;
}
quantity =
@@ -5886,7 +5929,9 @@ where
if filled_qty >= requested_qty {
return None;
}
if gross_limit.is_some_and(|limit| price * requested_qty as f64 > limit + 1e-6) {
if gross_limit
.is_some_and(|limit| !Self::fixed_cash_fits(price * requested_qty as f64, limit))
{
Some("value budget limit")
} else if cash_limit.is_finite() {
Some("insufficient cash after fees")
@@ -6326,7 +6371,9 @@ where
break;
}
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))
{
budget_block_reason = Some("value budget limit");
take_qty = self.decrement_order_quantity(
take_qty,
@@ -6339,7 +6386,11 @@ where
.cost_model
.calculate(snapshot.date, OrderSide::Buy, candidate_gross)
.total();
if candidate_gross + candidate_cost <= cash + 1e-6 {
let candidate_cash =
FixedMoney::checked_sum_f64([candidate_gross, candidate_cost])
.expect("buy cash must be finite fixed-point money")
.to_f64();
if Self::fixed_cash_fits(candidate_cash, cash) {
break;
}
budget_block_reason = Some("insufficient cash after fees");
@@ -6586,6 +6637,7 @@ mod tests {
IntradayExecutionQuote, PriceField,
};
use crate::events::{OrderSide, OrderStatus};
use crate::fixed_point::FixedMoney;
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::risk_control::FidcRiskControlConfig;
@@ -7650,7 +7702,7 @@ mod tests {
1_000,
10.0,
);
portfolio.apply_cash_delta(-10_000.0);
portfolio.apply_cash_delta(-10_000.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -7708,7 +7760,7 @@ mod tests {
10_000,
10.0,
);
portfolio.apply_cash_delta(-100_000.0);
portfolio.apply_cash_delta(-100_000.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -7766,7 +7818,7 @@ mod tests {
1_000,
10.0,
);
portfolio.apply_cash_delta(-10_000.0);
portfolio.apply_cash_delta(-10_000.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -7856,7 +7908,7 @@ mod tests {
1_000,
10.0,
);
portfolio.apply_cash_delta(-10_000.0);
portfolio.apply_cash_delta(-10_000.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -7943,7 +7995,7 @@ mod tests {
10_000,
10.0,
);
portfolio.apply_cash_delta(-100_000.0);
portfolio.apply_cash_delta(-100_000.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -9122,7 +9174,7 @@ mod tests {
.expect("valid dataset");
let mut portfolio = PortfolioState::new(1_000_000.0);
portfolio.position_mut(symbol).buy(prev_date, 72_600, 4.0);
portfolio.apply_cash_delta(-290_400.0);
portfolio.apply_cash_delta(-290_400.0).unwrap();
let mut report = BrokerExecutionReport::default();
broker
@@ -9166,13 +9218,10 @@ mod tests {
let date = chrono::NaiveDate::from_ymd_opt(2023, 5, 8).expect("valid date");
let symbol = "603101.SH";
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel {
commission_rate: 0.0003,
stamp_tax_rate_before_change: 0.0005,
stamp_tax_rate_after_change: 0.0005,
minimum_commission: 5.0,
..ChinaAShareCostModel::default()
},
ChinaAShareCostModel::default()
.with_commission_rate(0.0003)
.with_stamp_tax_rates(0.0005, 0.0005)
.with_minimum_commission(5.0),
ChinaEquityRuleHooks,
PriceField::Last,
)
@@ -9515,7 +9564,20 @@ mod tests {
let fill = report.fill_events.first().expect("fill event");
assert_eq!(fill.quantity, 17_400);
assert!(fill.gross_amount + fill.commission <= value_budget + 1e-6);
let cash_out = FixedMoney::checked_sum_f64([
fill.gross_amount,
fill.commission,
fill.stamp_tax,
fill.transfer_fee,
])
.unwrap()
.to_f64();
assert!(
BrokerSimulator::<ChinaAShareCostModel, ChinaEquityRuleHooks>::fixed_cash_fits(
cash_out,
value_budget
)
);
assert!((fill.price - 7.15428).abs() < 1e-6);
}