fix(stock-pool): separate cumulative condition facts from quote capacity

This commit is contained in:
boris
2026-09-12 18:50:12 +08:00
parent 61bd14d001
commit 875e31f71f
12 changed files with 407 additions and 133 deletions
+37 -13
View File
@@ -47,8 +47,10 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
data: &DataSet,
symbols: &BTreeSet<String>,
execution_clock: Option<NaiveDateTime>,
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> {
symbols
cumulative_conditions: bool,
) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
let mut unavailable = Vec::new();
let quotes = symbols
.iter()
.map(|symbol| {
let snapshot = data.market(date, symbol).ok_or_else(|| {
@@ -134,11 +136,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
None,
calibration.as_ref(),
)?;
let totals = if cumulative_conditions {
match data.execution_session_totals(symbol, clock) {
Ok(totals) => Some(totals),
Err(reason) => { unavailable.push(reason); None }
}
} else { None };
(
quote.last_price,
snapshot.prev_close,
Some(quote.volume_delta as f64),
Some(quote.amount_delta),
totals.map(|total| total.0),
totals.map(|total| total.1),
Some(quote.bid1),
Some(quote.ask1),
buy,
@@ -153,13 +161,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
}
// A daily open does not reveal the session's volume/turnover.
let completed = self.effective_execution_price_field(date) == PriceField::Close;
let totals = if cumulative_conditions && !completed {
let at = execution_clock.unwrap_or_else(|| date.and_hms_opt(9,30,0).unwrap());
match data.execution_session_totals(symbol, at) {
Ok(totals) => Some(totals),
Err(reason) => { unavailable.push(reason); None }
}
} else { None };
let amount = if completed && cumulative_conditions {
data.factor(date, symbol).and_then(|row| row.extra_factors.get("amount")).copied()
.map(|value| decimal(value, "amount")).transpose()?
} else { totals.map(|total| total.1) };
(
price,
snapshot.prev_close,
completed.then_some(snapshot.volume as f64),
if completed { Some(Decimal::from(snapshot.volume)) } else { totals.map(|total| total.0) },
amount,
None,
None,
Some(price),
Some(price),
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
)
@@ -168,8 +187,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
symbol: symbol.clone(),
last_price: decimal(price, "price")?,
prev_close: Some(decimal(prev, "prev_close")?),
volume: volume.map(|v| decimal(v, "volume")).transpose()?,
turnover: amount.map(|v| decimal(v, "amount")).transpose()?,
volume,
turnover: amount,
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
@@ -182,7 +201,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
})
})
.collect()
.collect::<Result<Vec<_>, BacktestError>>()?;
Ok((quotes, unavailable))
}
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
@@ -328,8 +348,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
fallback_references.insert(symbol.clone(), reference);
}
}
let quotes =
self.pool_quote_inputs(date, data, &quote_scope, *global_execution_cursor)?;
let (quotes, unavailable) = self.pool_quote_inputs(date, data, &quote_scope, *global_execution_cursor,
crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))?;
let positions = pool_positions(portfolio, date)?;
let execution_state = portfolio
.stock_pool_execution_state(&contract.pool_id)
@@ -448,7 +468,11 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
Decimal::ZERO,
Some(&fee),
)
.map_err(BacktestError::Execution)?;
.map_err(|error| BacktestError::Execution(if !unavailable.is_empty()
&& (error.contains("requires volume") || error.contains("requires amount")) {
format!("{error}; {}", unavailable.join("; "))
} else { error }))?;
report.diagnostics.extend(unavailable.into_iter().map(|reason| format!("stock_pool_quote_fact_unavailable {reason}")));
let mut updated = execution_state
.record_plan(contract.signal_date, &contract.generation, &plan)
.map_err(BacktestError::Execution)?;