将股票执行资金切换为定点账本
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user