保留模拟器失败调用前的委托与执行状态

This commit is contained in:
boris
2026-09-15 01:28:38 +08:00
parent 4c6147e2ee
commit 695fdee4b8
6 changed files with 776 additions and 30 deletions
+152 -28
View File
@@ -100,7 +100,7 @@ struct QuoteLiquidityConsumption {
quantity: u32,
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
struct IntradayExecutionLedger {
cursors: BTreeMap<String, NaiveDateTime>,
depth_consumption: BTreeMap<String, [Option<QuoteDepthConsumption>; 2]>,
@@ -235,7 +235,7 @@ enum BrokerCallbackPhase {
BeforeStrategy,
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
struct BrokerExecutionSession {
date: Option<NaiveDate>,
intraday_turnover: BTreeMap<String, u32>,
@@ -441,6 +441,43 @@ impl<T: Copy> Drop for RestoreCell<'_, T> {
fn drop(&mut self) { self.0.set(self.1); }
}
struct RestoreRefCell<'a, T>(&'a RefCell<T>, Option<T>);
impl<T> Drop for RestoreRefCell<'_, T> {
fn drop(&mut self) {
if let Some(value) = self.1.take() { self.0.replace(value); }
}
}
macro_rules! execution_context_checkpoint {
($($field:ident : $kind:ty),* $(,)?) => {
struct BrokerExecutionContext { $($field: $kind),* }
impl BrokerExecutionContext {
fn capture<C, R>(broker: &BrokerSimulator<C, R>) -> Self {
Self { $($field: broker.$field.get()),* }
}
fn restore<C, R>(self, broker: &BrokerSimulator<C, R>) {
$(broker.$field.set(self.$field);)*
}
}
};
}
execution_context_checkpoint! {
runtime_etf_daily_open: bool,
runtime_stock_pool_followup: bool,
runtime_intraday_start_time: Option<NaiveTime>,
runtime_intraday_end_time: Option<NaiveTime>,
runtime_execution_clock: Option<NaiveTime>,
runtime_callback_phase: BrokerCallbackPhase,
runtime_algo_schedule: Option<AlgoExecutionRequest>,
runtime_unprocessed_algorithm_cash: FixedMoney,
runtime_decision_date: Option<NaiveDate>,
runtime_order_created_date: Option<NaiveDate>,
runtime_resting_order_origin: Option<RestingOrderOrigin>,
runtime_decision_total_equity: Option<f64>,
runtime_target_position_limit: Option<usize>,
runtime_time_in_force: Option<OrderTimeInForce>,
}
pub struct BrokerSimulator<C, R> {
historical_etf_open_fallback: bool,
verified_etf_minute_absences: RefCell<BTreeSet<(NaiveDate, String)>>,
@@ -485,9 +522,96 @@ pub struct BrokerSimulator<C, R> {
next_order_id: Cell<u64>,
open_orders: RefCell<Vec<OpenOrder>>,
execution_session: RefCell<BrokerExecutionSession>,
execution_transaction_depth: Cell<usize>,
}
/// Only unpublished simulator state is transactional. Broker observations and
/// results returned successfully by earlier calls are outside this checkpoint.
struct BrokerExecutionCheckpoint {
portfolio: crate::portfolio::PortfolioCheckpoint,
orders: Vec<OpenOrder>,
etf_targets: crate::etf_execution::DeferredEtfTargets,
pool_targets: BTreeMap<String, stock_pool::DeferredStockPoolExecution>,
sold: BTreeMap<NaiveDate, BTreeSet<String>>,
session: BrokerExecutionSession,
next_order_id: u64,
context: BrokerExecutionContext,
}
impl BrokerExecutionCheckpoint {
fn capture<C, R>(broker: &BrokerSimulator<C, R>, portfolio: &PortfolioState, symbols: Option<&BTreeSet<String>>) -> Self {
Self {
portfolio: portfolio.checkpoint(symbols), orders: broker.open_orders.borrow().clone(),
etf_targets: broker.deferred_etf_targets.borrow().clone(),
pool_targets: broker.deferred_stock_pools.borrow().clone(),
sold: broker.same_day_sold_symbols.borrow().clone(),
session: broker.execution_session.borrow().clone(),
next_order_id: broker.next_order_id.get(),
context: BrokerExecutionContext::capture(broker),
}
}
fn restore<C, R>(self, broker: &BrokerSimulator<C, R>, portfolio: &mut PortfolioState) {
self.portfolio.restore(portfolio);
*broker.open_orders.borrow_mut() = self.orders;
*broker.deferred_etf_targets.borrow_mut() = self.etf_targets;
*broker.deferred_stock_pools.borrow_mut() = self.pool_targets;
*broker.same_day_sold_symbols.borrow_mut() = self.sold;
*broker.execution_session.borrow_mut() = self.session;
broker.next_order_id.set(self.next_order_id);
self.context.restore(broker);
}
}
struct BrokerExecutionTransaction<'a, C, R> {
broker: &'a BrokerSimulator<C, R>,
portfolio: &'a mut PortfolioState,
checkpoint: Option<BrokerExecutionCheckpoint>,
}
impl<C, R> Drop for BrokerExecutionTransaction<'_, C, R> {
fn drop(&mut self) {
if let Some(checkpoint) = self.checkpoint.take() {
checkpoint.restore(self.broker, self.portfolio);
}
}
}
impl<C, R> BrokerSimulator<C, R> {
fn execution_transaction<F>(&self, portfolio: &mut PortfolioState, needed: bool, symbols: Option<&BTreeSet<String>>, execute: F)
-> Result<BrokerExecutionReport, BacktestError>
where F: FnOnce(&mut PortfolioState) -> Result<BrokerExecutionReport, BacktestError> {
if !needed || self.execution_transaction_depth.get() > 0 { return execute(portfolio); }
let checkpoint = BrokerExecutionCheckpoint::capture(self, portfolio, symbols);
let mut transaction = BrokerExecutionTransaction { broker: self, portfolio, checkpoint: Some(checkpoint) };
let _depth = RestoreCell(&self.execution_transaction_depth,
self.execution_transaction_depth.replace(1));
let result = execute(transaction.portfolio);
if result.is_ok() { transaction.checkpoint = None; }
result
}
fn checkpoint_symbols(&self, decision: &StrategyDecision) -> Option<BTreeSet<String>> {
if decision.rebalance || !self.deferred_stock_pools.borrow().is_empty() { return None; }
let mut symbols = self.open_orders.borrow().iter().map(|order| order.symbol.clone()).collect::<BTreeSet<_>>();
symbols.extend(decision.exit_symbols.iter().cloned());
for intent in &decision.order_intents {
let symbol = match intent.unwrapped() {
OrderIntent::Shares { symbol, .. } | OrderIntent::LimitShares { symbol, .. }
| OrderIntent::Lots { symbol, .. } | OrderIntent::LimitLots { symbol, .. }
| OrderIntent::TargetShares { symbol, .. } | OrderIntent::LimitTargetShares { symbol, .. }
| OrderIntent::Value { symbol, .. } | OrderIntent::LimitValue { symbol, .. }
| OrderIntent::TargetValue { symbol, .. } | OrderIntent::LimitTargetValue { symbol, .. }
| OrderIntent::TimedTargetValue { symbol, .. } | OrderIntent::AlgoValue { symbol, .. }
| OrderIntent::Percent { symbol, .. } | OrderIntent::LimitPercent { symbol, .. }
| OrderIntent::TargetPercent { symbol, .. } | OrderIntent::LimitTargetPercent { symbol, .. } => symbol,
// Unknown/new/whole-portfolio controls must retain everything.
_ => return None,
};
symbols.insert(symbol.clone());
}
Some(symbols)
}
pub fn new(cost_model: C, rules: R) -> Self {
Self {
historical_etf_open_fallback: false,
@@ -531,6 +655,7 @@ impl<C, R> BrokerSimulator<C, R> {
runtime_target_position_limit: Cell::new(None),
runtime_time_in_force: Cell::new(None),
next_order_id: Cell::new(1),
execution_transaction_depth: Cell::new(0),
open_orders: RefCell::new(Vec::new()),
execution_session: RefCell::new(BrokerExecutionSession::default()),
}
@@ -583,6 +708,7 @@ impl<C, R> BrokerSimulator<C, R> {
runtime_target_position_limit: Cell::new(None),
runtime_time_in_force: Cell::new(None),
next_order_id: Cell::new(1),
execution_transaction_depth: Cell::new(0),
open_orders: RefCell::new(Vec::new()),
execution_session: RefCell::new(BrokerExecutionSession::default()),
}
@@ -1602,30 +1728,22 @@ where
data: &DataSet,
decision: &StrategyDecision,
) -> Result<BrokerExecutionReport, BacktestError> {
let previous_decision_date = self.runtime_decision_date.get();
let previous_buy_denials = self.runtime_buy_denials.replace(decision.buy_denials.clone());
let _buy_denials = RestoreRefCell(&self.runtime_buy_denials,
Some(self.runtime_buy_denials.replace(decision.buy_denials.clone())));
let protection_denials = |scope| decision.risk_decisions.iter()
.filter(|row| !row.accepted && row.stage == "automatic_trade_protection" && row.scope == scope)
.map(|row| (row.symbol.clone(), row.reason.clone())).collect();
let previous_auto_buy_denials = self.runtime_auto_buy_denials.replace(protection_denials(crate::risk_control::RiskCheckScope::Buy));
let previous_auto_sell_denials = self.runtime_auto_sell_denials.replace(protection_denials(crate::risk_control::RiskCheckScope::Sell));
let previous_order_created_date = self.runtime_order_created_date.get();
let previous_decision_total_equity = self.runtime_decision_total_equity.get();
self.runtime_decision_date.set(Some(decision_date));
self.runtime_order_created_date
.set(Some(order_created_date));
self.runtime_decision_total_equity
.set(decision_total_equity.filter(|equity| equity.is_finite() && *equity >= 0.0));
let result = self.execute_with_runtime_dates(date, portfolio, data, decision);
self.runtime_buy_denials.replace(previous_buy_denials);
self.runtime_auto_buy_denials.replace(previous_auto_buy_denials);
self.runtime_auto_sell_denials.replace(previous_auto_sell_denials);
self.runtime_decision_date.set(previous_decision_date);
self.runtime_order_created_date
.set(previous_order_created_date);
self.runtime_decision_total_equity
.set(previous_decision_total_equity);
result
let _auto_buy = RestoreRefCell(&self.runtime_auto_buy_denials, Some(self.runtime_auto_buy_denials
.replace(protection_denials(crate::risk_control::RiskCheckScope::Buy))));
let _auto_sell = RestoreRefCell(&self.runtime_auto_sell_denials, Some(self.runtime_auto_sell_denials
.replace(protection_denials(crate::risk_control::RiskCheckScope::Sell))));
let _decision_date = RestoreCell(&self.runtime_decision_date,
self.runtime_decision_date.replace(Some(decision_date)));
let _created_date = RestoreCell(&self.runtime_order_created_date,
self.runtime_order_created_date.replace(Some(order_created_date)));
let _equity = RestoreCell(&self.runtime_decision_total_equity,
self.runtime_decision_total_equity.replace(decision_total_equity.filter(|equity| equity.is_finite() && *equity >= 0.0)));
self.execute_with_runtime_dates(date, portfolio, data, decision)
}
fn execute_with_runtime_dates(
@@ -1638,11 +1756,16 @@ where
if self.volume_limit {
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
}
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
session.activate(date);
let result = self.execute_with_daily_session(date, portfolio, data, decision, &mut session);
*self.execution_session.borrow_mut() = session;
result
self.execution_session.borrow_mut().activate(date);
let may_execute = self.has_open_orders() || !self.deferred_stock_pools.borrow().is_empty()
|| decision.rebalance || !decision.order_intents.is_empty() || !decision.exit_symbols.is_empty();
let symbols = may_execute.then(|| self.checkpoint_symbols(decision)).flatten();
self.execution_transaction(portfolio, may_execute, symbols.as_ref(), |portfolio| {
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
let result = self.execute_with_daily_session(date, portfolio, data, decision, &mut session);
*self.execution_session.borrow_mut() = session;
result
})
}
fn execute_with_daily_session(
@@ -8749,6 +8872,7 @@ mod tests {
use crate::strategy::{AlgoOrderStyle, OrderIntent, OrderTimeInForce, StrategyDecision};
include!("broker_stock_pool_batch_tests.rs");
include!("broker_order_recovery_tests.rs");
#[test]
fn queued_order_retains_the_real_creation_clock_when_retried() {