修复股票池卖单回报后未继续执行买入阶段

This commit is contained in:
boris
2026-09-13 23:43:26 +08:00
parent 53af3a6a85
commit c98bcc3eb2
5 changed files with 844 additions and 18 deletions
+33 -10
View File
@@ -427,6 +427,8 @@ pub struct BrokerSimulator<C, R> {
verified_etf_minute_absences: RefCell<BTreeSet<(NaiveDate, String)>>,
runtime_etf_daily_open: Cell<bool>,
deferred_etf_targets: RefCell<crate::etf_execution::DeferredEtfTargets>,
deferred_stock_pools: RefCell<BTreeMap<String, stock_pool::DeferredStockPoolExecution>>,
runtime_stock_pool_followup: Cell<bool>,
cost_model: C,
rules: R,
board_lot_size: u32,
@@ -469,6 +471,8 @@ impl<C, R> BrokerSimulator<C, R> {
verified_etf_minute_absences: RefCell::new(BTreeSet::new()),
runtime_etf_daily_open: Cell::new(false),
deferred_etf_targets: RefCell::new(Default::default()),
deferred_stock_pools: RefCell::new(BTreeMap::new()),
runtime_stock_pool_followup: Cell::new(false),
cost_model,
rules,
board_lot_size: 100,
@@ -515,6 +519,8 @@ impl<C, R> BrokerSimulator<C, R> {
verified_etf_minute_absences: RefCell::new(BTreeSet::new()),
runtime_etf_daily_open: Cell::new(false),
deferred_etf_targets: RefCell::new(Default::default()),
deferred_stock_pools: RefCell::new(BTreeMap::new()),
runtime_stock_pool_followup: Cell::new(false),
cost_model,
rules,
board_lot_size: 100,
@@ -778,7 +784,7 @@ impl<C, R> BrokerSimulator<C, R> {
if self.runtime_etf_daily_open.get() { return PriceField::Open; }
if self.is_post_close_fixed_price(date) {
PriceField::Close
} else if self.resting_daily_open_order() {
} else if self.resting_daily_open_order() || (self.runtime_stock_pool_followup.get() && self.matching_type == MatchingType::NextBarOpen) {
PriceField::Last
} else {
self.execution_price_field
@@ -901,6 +907,12 @@ impl<C, R> BrokerSimulator<C, R> {
!self.open_orders.borrow().is_empty()
}
fn new_open_order_submission_time(&self) -> Option<NaiveTime> {
if self.matching_type == MatchingType::NextBarOpen && !self.runtime_stock_pool_followup.get() {
NaiveTime::from_hms_opt(9, 30, 0)
} else { self.order_origin().1 }
}
fn resting_order_session_close(&self, date: NaiveDate, order: &OpenOrder) -> NaiveTime {
let post_close = self.execution_phase_for_submission(date, order.order_created_date, order.submission_time)
== EquityExecutionPhase::PostCloseFixedPrice;
@@ -1479,7 +1491,7 @@ where
match algo_request.map(|request| request.style) {
Some(AlgoExecutionStyle::Vwap) => MatchingType::Vwap,
Some(AlgoExecutionStyle::Twap) => MatchingType::Twap,
None if self.resting_daily_open_order() => MatchingType::CurrentBarClose,
None if self.resting_daily_open_order() || (self.runtime_stock_pool_followup.get() && self.matching_type == MatchingType::NextBarOpen) => MatchingType::CurrentBarClose,
None => self.matching_type,
}
}
@@ -1597,6 +1609,13 @@ where
session: &mut BrokerExecutionSession,
) -> Result<BrokerExecutionReport, BacktestError> {
let mut report = BrokerExecutionReport::default();
// A fresh strategy intent supersedes any unsubmitted remainder before
// old order reports can resume it. Already submitted orders are kept.
for intent in &decision.order_intents {
if let OrderIntent::StockPool { contract } = intent.unwrapped() {
self.deferred_stock_pools.borrow_mut().remove(&contract.pool_id);
}
}
self.process_open_orders(
date,
portfolio,
@@ -1607,6 +1626,7 @@ where
&mut session.commission_state,
&mut report,
)?;
self.resume_stock_pool_executions(date, portfolio, data, session, &mut report)?;
if !decision.order_intents.is_empty() {
let mut ordered_intents = decision.order_intents.iter().collect::<Vec<_>>();
if self.effective_rebalance_cash_mode() != RebalanceCashMode::PreOpenCash
@@ -4149,7 +4169,7 @@ where
side: OrderSide,
algo_request: Option<&AlgoExecutionRequest>,
) -> f64 {
if self.matching_type == MatchingType::NextBarOpen && !self.resting_daily_open_order() && algo_request.is_none() {
if self.matching_type == MatchingType::NextBarOpen && !self.resting_daily_open_order() && !self.runtime_stock_pool_followup.get() && algo_request.is_none() {
return self.execution_limit_check_price(snapshot, side);
}
let matching_type = self.matching_type_for_algo_request(algo_request);
@@ -4735,7 +4755,7 @@ where
if Self::keeps_remainder_open(remainder_policy) {
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -4826,7 +4846,7 @@ where
.unwrap_or("no sellable quantity");
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -4999,7 +5019,7 @@ where
{
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -5171,7 +5191,7 @@ where
if keep_open {
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -6559,7 +6579,7 @@ where
if Self::keeps_remainder_open(remainder_policy) {
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -6799,7 +6819,7 @@ where
{
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -6973,7 +6993,7 @@ where
if keep_open {
self.upsert_open_order(OpenOrder {
order_id,
submission_time: if self.matching_type == MatchingType::NextBarOpen { NaiveTime::from_hms_opt(9,30,0) } else { self.order_origin().1 },
submission_time: self.new_open_order_submission_time(),
accepted_date: self.accepted_order_date(date),
decision_date: Some(self.current_decision_date(date)),
order_created_date: Some(self.current_order_created_date(date)),
@@ -8110,6 +8130,7 @@ where
pub(crate) fn matching_type_uses_intraday_quotes(&self) -> bool {
if self.runtime_etf_daily_open.get() { return false; }
if self.runtime_stock_pool_followup.get() { return true; }
if self.resting_daily_open_order() { return true; }
matches!(
self.matching_type,
@@ -8252,6 +8273,8 @@ mod tests {
use crate::rules::ChinaEquityRuleHooks;
use crate::strategy::{AlgoOrderStyle, OrderIntent, OrderTimeInForce, StrategyDecision};
include!("broker_stock_pool_batch_tests.rs");
fn test_open_order(order_id: u64) -> OpenOrder {
OpenOrder {
order_id,