fix(stock-pool): execute verified ETF daily fallbacks with frozen next-open targets
This commit is contained in:
@@ -423,6 +423,10 @@ struct AlgoExecutionRequest {
|
||||
}
|
||||
|
||||
pub struct BrokerSimulator<C, R> {
|
||||
historical_etf_open_fallback: bool,
|
||||
verified_etf_minute_absences: RefCell<BTreeSet<(NaiveDate, String)>>,
|
||||
runtime_etf_daily_open: Cell<bool>,
|
||||
deferred_etf_targets: RefCell<crate::etf_execution::DeferredEtfTargets>,
|
||||
cost_model: C,
|
||||
rules: R,
|
||||
board_lot_size: u32,
|
||||
@@ -461,6 +465,10 @@ pub struct BrokerSimulator<C, R> {
|
||||
impl<C, R> BrokerSimulator<C, R> {
|
||||
pub fn new(cost_model: C, rules: R) -> Self {
|
||||
Self {
|
||||
historical_etf_open_fallback: false,
|
||||
verified_etf_minute_absences: RefCell::new(BTreeSet::new()),
|
||||
runtime_etf_daily_open: Cell::new(false),
|
||||
deferred_etf_targets: RefCell::new(Default::default()),
|
||||
cost_model,
|
||||
rules,
|
||||
board_lot_size: 100,
|
||||
@@ -503,6 +511,10 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
execution_price_field: PriceField,
|
||||
) -> Self {
|
||||
Self {
|
||||
historical_etf_open_fallback: false,
|
||||
verified_etf_minute_absences: RefCell::new(BTreeSet::new()),
|
||||
runtime_etf_daily_open: Cell::new(false),
|
||||
deferred_etf_targets: RefCell::new(Default::default()),
|
||||
cost_model,
|
||||
rules,
|
||||
board_lot_size: 100,
|
||||
@@ -549,6 +561,40 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Historical stock-pool adapter only. Online runtimes never enable this.
|
||||
pub fn with_historical_etf_open_fallback(mut self, enabled: bool) -> Self {
|
||||
self.historical_etf_open_fallback = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn requires_etf_absence_check(&self, data: &DataSet, symbol: &str) -> bool {
|
||||
self.historical_etf_open_fallback && data.instrument(symbol).is_some_and(|v| v.is_exchange_traded_fund())
|
||||
}
|
||||
|
||||
pub(crate) fn record_complete_etf_minute_query(&self, date: NaiveDate, data: &DataSet, symbols: &[String]) {
|
||||
for symbol in symbols {
|
||||
if self.requires_etf_absence_check(data, symbol) && data.execution_quotes_on(date, symbol).is_empty() {
|
||||
self.verified_etf_minute_absences.borrow_mut().insert((date, symbol.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_verified_etf_minute_absence(&self, date: NaiveDate, symbol: &str) -> bool {
|
||||
self.historical_etf_open_fallback && self.verified_etf_minute_absences.borrow().contains(&(date, symbol.to_string()))
|
||||
}
|
||||
|
||||
fn with_etf_daily_open<T>(&self, operation: impl FnOnce() -> Result<T, BacktestError>) -> Result<T, BacktestError> {
|
||||
if self.liquidity_limit {
|
||||
return Err(BacktestError::Execution("etf_daily_open_fallback: historical opening depth is unavailable; cannot satisfy liquidity_limit".into()));
|
||||
}
|
||||
self.volume_capacity_mode.validate(self.volume_limit, false)
|
||||
.map_err(|error| BacktestError::Execution(format!("etf_daily_open_fallback: {error}")))?;
|
||||
let prior = self.runtime_etf_daily_open.replace(true);
|
||||
let result = operation();
|
||||
self.runtime_etf_daily_open.set(prior);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn capacity_audit_summary(&self) -> CapacityAuditSummary {
|
||||
CapacityAuditSummary { mode: self.volume_capacity_mode, enabled: self.volume_limit,
|
||||
participation_rate: self.volume_percent, ..Default::default() }
|
||||
@@ -729,6 +775,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
}
|
||||
|
||||
fn effective_execution_price_field(&self, date: NaiveDate) -> PriceField {
|
||||
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() {
|
||||
@@ -910,6 +957,7 @@ where
|
||||
symbol: &str,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
) -> f64 {
|
||||
if self.runtime_etf_daily_open.get() { return snapshot.open; }
|
||||
if self.is_post_close_fixed_price(date) {
|
||||
return snapshot.close;
|
||||
}
|
||||
@@ -1268,6 +1316,7 @@ where
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
) -> f64 {
|
||||
if self.runtime_etf_daily_open.get() { return snapshot.open; }
|
||||
if self.is_post_close_fixed_price(snapshot.date) {
|
||||
return snapshot.close;
|
||||
}
|
||||
@@ -1426,6 +1475,7 @@ where
|
||||
&self,
|
||||
algo_request: Option<&AlgoExecutionRequest>,
|
||||
) -> MatchingType {
|
||||
if self.runtime_etf_daily_open.get() && algo_request.is_none() { return MatchingType::NextBarOpen; }
|
||||
match algo_request.map(|request| request.style) {
|
||||
Some(AlgoExecutionStyle::Vwap) => MatchingType::Vwap,
|
||||
Some(AlgoExecutionStyle::Twap) => MatchingType::Twap,
|
||||
@@ -4082,6 +4132,7 @@ where
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
side: OrderSide,
|
||||
) -> f64 {
|
||||
if self.runtime_etf_daily_open.get() { return snapshot.open; }
|
||||
match (self.execution_price_field, side) {
|
||||
(PriceField::Last, _) => snapshot.price(PriceField::Last),
|
||||
(_, OrderSide::Buy) => snapshot.buy_price(self.execution_price_field),
|
||||
@@ -8058,6 +8109,7 @@ where
|
||||
}
|
||||
|
||||
pub(crate) fn matching_type_uses_intraday_quotes(&self) -> bool {
|
||||
if self.runtime_etf_daily_open.get() { return false; }
|
||||
if self.resting_daily_open_order() { return true; }
|
||||
matches!(
|
||||
self.matching_type,
|
||||
|
||||
Reference in New Issue
Block a user