保留模拟器失败调用前的委托与执行状态
This commit is contained in:
+152
-28
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user