Compare commits

..

5 Commits

15 changed files with 5499 additions and 183 deletions
+2 -2
View File
@@ -8324,7 +8324,7 @@ mod tests {
fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote { fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
@@ -11729,7 +11729,7 @@ mod tests {
lower_limit: 5.27, lower_limit: 5.27,
price_tick: 0.01, price_tick: 0.01,
}; };
let quote = IntradayExecutionQuote { let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"),
+37 -13
View File
@@ -47,8 +47,10 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
data: &DataSet, data: &DataSet,
symbols: &BTreeSet<String>, symbols: &BTreeSet<String>,
execution_clock: Option<NaiveDateTime>, execution_clock: Option<NaiveDateTime>,
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> { cumulative_conditions: bool,
symbols ) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
let mut unavailable = Vec::new();
let quotes = symbols
.iter() .iter()
.map(|symbol| { .map(|symbol| {
let snapshot = data.market(date, symbol).ok_or_else(|| { let snapshot = data.market(date, symbol).ok_or_else(|| {
@@ -134,11 +136,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
None, None,
calibration.as_ref(), 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, quote.last_price,
snapshot.prev_close, snapshot.prev_close,
Some(quote.volume_delta as f64), totals.map(|total| total.0),
Some(quote.amount_delta), totals.map(|total| total.1),
Some(quote.bid1), Some(quote.bid1),
Some(quote.ask1), Some(quote.ask1),
buy, buy,
@@ -153,13 +161,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
} }
// A daily open does not reveal the session's volume/turnover. // A daily open does not reveal the session's volume/turnover.
let completed = self.effective_execution_price_field(date) == PriceField::Close; 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, price,
snapshot.prev_close, 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, None,
Some(price),
Some(price),
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?, self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, 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(), symbol: symbol.clone(),
last_price: decimal(price, "price")?, last_price: decimal(price, "price")?,
prev_close: Some(decimal(prev, "prev_close")?), prev_close: Some(decimal(prev, "prev_close")?),
volume: volume.map(|v| decimal(v, "volume")).transpose()?, volume,
turnover: amount.map(|v| decimal(v, "amount")).transpose()?, turnover: amount,
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?, bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?, ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")), 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")?), 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> { 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); fallback_references.insert(symbol.clone(), reference);
} }
} }
let quotes = let (quotes, unavailable) = self.pool_quote_inputs(date, data, &quote_scope, *global_execution_cursor,
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 positions = pool_positions(portfolio, date)?;
let execution_state = portfolio let execution_state = portfolio
.stock_pool_execution_state(&contract.pool_id) .stock_pool_execution_state(&contract.pool_id)
@@ -448,7 +468,11 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
Decimal::ZERO, Decimal::ZERO,
Some(&fee), 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 let mut updated = execution_state
.record_plan(contract.signal_date, &contract.generation, &plan) .record_plan(contract.signal_date, &contract.generation, &plan)
.map_err(BacktestError::Execution)?; .map_err(BacktestError::Execution)?;
+103 -46
View File
@@ -284,6 +284,8 @@ pub struct CorporateAction {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntradayExecutionQuote { pub struct IntradayExecutionQuote {
#[serde(default)]
pub observation_kind: QuoteObservationKind,
#[serde(with = "date_format")] #[serde(with = "date_format")]
pub date: NaiveDate, pub date: NaiveDate,
pub symbol: String, pub symbol: String,
@@ -301,6 +303,14 @@ pub struct IntradayExecutionQuote {
pub trading_phase: Option<String>, pub trading_phase: Option<String>,
} }
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuoteObservationKind {
#[default]
Unspecified,
MinuteBar,
}
/// Sparse same-day fields layered onto an already-built immutable daily panel. /// Sparse same-day fields layered onto an already-built immutable daily panel.
/// ///
/// These fields do not participate in daily price series, adjustment series, /// These fields do not participate in daily price series, adjustment series,
@@ -1407,6 +1417,7 @@ pub struct DataSet {
corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>, corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>,
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>, execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
execution_quote_dates: Arc<Vec<NaiveDate>>, execution_quote_dates: Arc<Vec<NaiveDate>>,
condition_totals: Arc<std::sync::Mutex<crate::stock_pool_quote_facts::SessionTotalsCache>>,
order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>, order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>,
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>, benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>, market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
@@ -1575,48 +1586,15 @@ impl DataSet {
benchmark_by_date: BTreeMap::new(), benchmark_by_date: BTreeMap::new(),
corporate_actions_by_date: BTreeMap::new(), corporate_actions_by_date: BTreeMap::new(),
}; };
for mut bundle in bundles { // Indexed collection retains chronological error precedence while each
// worker validates and normalizes only its owned day buffers.
let prepared = bundles
.into_par_iter()
.map(normalize_daily_snapshot_bundle)
.collect::<Vec<_>>();
for bundle in prepared {
let bundle = bundle?;
let date = bundle.date; let date = bundle.date;
if bundle.benchmark.date != date {
return Err(DataSetError::InvalidDailyBundleComponentDate {
kind: "benchmark",
bundle_date: date,
row_date: bundle.benchmark.date,
symbol: bundle.benchmark.benchmark.clone(),
});
}
validate_daily_bundle_component_dates(
&bundle.market,
date,
"market",
|row| row.date,
|row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.factors,
date,
"factor",
|row| row.date,
|row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.candidates,
date,
"candidate",
|row| row.date,
|row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.corporate_actions,
date,
"corporate_action",
|row| row.date,
|row| row.symbol.as_str(),
)?;
sort_rows_by_symbol_if_needed(&mut bundle.market, |row| row.symbol.as_str());
bundle.factors = normalize_factor_snapshots(bundle.factors)?;
sort_rows_by_symbol_if_needed(&mut bundle.factors, |row| row.symbol.as_str());
sort_rows_by_symbol_if_needed(&mut bundle.candidates, |row| row.symbol.as_str());
if !bundle.market.is_empty() { if !bundle.market.is_empty() {
grouped.market_by_date.insert(date, bundle.market); grouped.market_by_date.insert(date, bundle.market);
} }
@@ -1941,6 +1919,7 @@ impl DataSet {
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date), candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
corporate_actions_by_date: Arc::new(corporate_actions_by_date), corporate_actions_by_date: Arc::new(corporate_actions_by_date),
execution_quotes_by_date: Arc::new(execution_quotes_by_date), execution_quotes_by_date: Arc::new(execution_quotes_by_date),
condition_totals: Arc::new(std::sync::Mutex::new(Default::default())),
execution_quote_dates: Arc::new(execution_quote_dates), execution_quote_dates: Arc::new(execution_quote_dates),
order_book_depth_index: Arc::new(order_book_depth_index), order_book_depth_index: Arc::new(order_book_depth_index),
benchmark_by_date: Arc::new(benchmark_by_date), benchmark_by_date: Arc::new(benchmark_by_date),
@@ -2279,6 +2258,15 @@ impl DataSet {
.unwrap_or(&[]) .unwrap_or(&[])
} }
pub fn execution_session_totals(&self, symbol: &str, at: NaiveDateTime) -> Result<(rust_decimal::Decimal, rust_decimal::Decimal), String> {
let mut cache = self.condition_totals.lock().map_err(|_| "stock_pool_session_prefix_cache_poisoned")?;
if cache.date != Some(at.date()) {
cache.date = Some(at.date());
cache.symbols.clear();
}
cache.symbols.entry(symbol.into()).or_insert_with(|| crate::stock_pool_quote_facts::MinutePrefix::build(at.date(), symbol, self.execution_quotes_on(at.date(), symbol))).at(at)
}
pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool { pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool {
self.execution_quotes_by_date self.execution_quotes_by_date
.get(&date) .get(&date)
@@ -2451,6 +2439,7 @@ impl DataSet {
/// Replaces the run-local execution quote layer without touching the /// Replaces the run-local execution quote layer without touching the
/// immutable daily panel. /// immutable daily panel.
pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize { pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
let execution_quotes_by_date = build_execution_quote_index(quotes); let execution_quotes_by_date = build_execution_quote_index(quotes);
let quote_count = execution_quotes_by_date let quote_count = execution_quotes_by_date
.values() .values()
@@ -2466,6 +2455,7 @@ impl DataSet {
} }
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize { pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new(); let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
for quote in quotes { for quote in quotes {
grouped grouped
@@ -2566,6 +2556,7 @@ impl DataSet {
} }
pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize { pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date); let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date);
let Some(rows_by_symbol) = removed else { let Some(rows_by_symbol) = removed else {
return 0; return 0;
@@ -2578,6 +2569,7 @@ impl DataSet {
} }
pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize { pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
let row_count = self let row_count = self
.execution_quotes_by_date .execution_quotes_by_date
.get(&date) .get(&date)
@@ -4447,6 +4439,38 @@ fn normalize_history_frequency(frequency: &str) -> Option<String> {
} }
} }
fn normalize_daily_snapshot_bundle(
mut bundle: DailySnapshotBundle,
) -> Result<DailySnapshotBundle, DataSetError> {
let date = bundle.date;
if bundle.benchmark.date != date {
return Err(DataSetError::InvalidDailyBundleComponentDate {
kind: "benchmark",
bundle_date: date,
row_date: bundle.benchmark.date,
symbol: bundle.benchmark.benchmark.clone(),
});
}
validate_daily_bundle_component_dates(
&bundle.market, date, "market", |row| row.date, |row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.factors, date, "factor", |row| row.date, |row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.candidates, date, "candidate", |row| row.date, |row| row.symbol.as_str(),
)?;
validate_daily_bundle_component_dates(
&bundle.corporate_actions, date, "corporate_action", |row| row.date,
|row| row.symbol.as_str(),
)?;
sort_rows_by_symbol_if_needed(&mut bundle.market, |row| row.symbol.as_str());
bundle.factors = normalize_factor_snapshots(bundle.factors)?;
sort_rows_by_symbol_if_needed(&mut bundle.factors, |row| row.symbol.as_str());
sort_rows_by_symbol_if_needed(&mut bundle.candidates, |row| row.symbol.as_str());
Ok(bundle)
}
fn validate_daily_bundle_component_dates<T, D, S>( fn validate_daily_bundle_component_dates<T, D, S>(
rows: &[T], rows: &[T],
bundle_date: NaiveDate, bundle_date: NaiveDate,
@@ -5158,7 +5182,7 @@ mod tests {
&run_data.execution_quote_dates &run_data.execution_quote_dates
)); ));
run_data.add_execution_quotes(vec![IntradayExecutionQuote { run_data.add_execution_quotes(vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S") timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
.unwrap(), .unwrap(),
@@ -5301,7 +5325,7 @@ mod tests {
vec![benchmark_row("2025-01-02", 12.0)], vec![benchmark_row("2025-01-02", 12.0)],
) )
.unwrap(); .unwrap();
let quote = IntradayExecutionQuote { let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp, timestamp,
@@ -5403,7 +5427,7 @@ mod tests {
successor_cash: None, successor_cash: None,
}; };
corporate_actions.push(corporate_action.clone()); corporate_actions.push(corporate_action.clone());
execution_quotes.push(IntradayExecutionQuote { execution_quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbols[0].to_string(), symbol: symbols[0].to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -5517,6 +5541,39 @@ mod tests {
)); ));
} }
#[test]
fn parallel_daily_bundle_validation_keeps_earliest_error_and_component_order() {
let bundles = || (2..30).rev().map(|day| {
let date = NaiveDate::from_ymd_opt(2025, 1, day).unwrap();
let mut benchmark = benchmark_row("2025-01-01", 20.0);
benchmark.date = date;
DailySnapshotBundle {
date, benchmark,
market: vec![market_row("2025-01-01", 10.0, 100)],
factors: Vec::new(), candidates: Vec::new(), corporate_actions: Vec::new(),
}
}).collect::<Vec<_>>();
for threads in [1, 2, 8] {
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
for _ in 0..4 {
let result = pool.install(|| DataSet::from_daily_bundles_with_execution_quotes(
Vec::new(), bundles(), Vec::new(),
));
assert!(matches!(result, Err(DataSetError::InvalidDailyBundleComponentDate {
kind: "market", bundle_date, ..
}) if bundle_date == NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()));
}
let mut values = bundles();
values.last_mut().unwrap().benchmark.date = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
let result = pool.install(|| DataSet::from_daily_bundles_with_execution_quotes(
Vec::new(), values, Vec::new(),
));
assert!(matches!(result, Err(DataSetError::InvalidDailyBundleComponentDate {
kind: "benchmark", bundle_date, ..
}) if bundle_date == NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()));
}
}
#[test] #[test]
fn direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows() { fn direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows() {
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(); let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
@@ -6114,7 +6171,7 @@ mod tests {
vec![benchmark_row("2025-01-02", 12.0)], vec![benchmark_row("2025-01-02", 12.0)],
) )
.unwrap(); .unwrap();
let quote = |symbol: &str, time: &str| IntradayExecutionQuote { let quote = |symbol: &str, time: &str| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
timestamp: NaiveDateTime::parse_from_str( timestamp: NaiveDateTime::parse_from_str(
&format!("2025-01-02 {time}"), &format!("2025-01-02 {time}"),
@@ -6193,7 +6250,7 @@ mod tests {
#[test] #[test]
fn shared_execution_quote_release_does_not_clone_the_base_map() { fn shared_execution_quote_release_does_not_clone_the_base_map() {
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(); let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
let quote = IntradayExecutionQuote { let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S") timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
.unwrap(), .unwrap(),
+14 -5
View File
@@ -746,6 +746,15 @@ where
if self.execution_quote_loader.is_none() { if self.execution_quote_loader.is_none() {
return Ok(()); return Ok(());
} }
let cumulative_conditions = decision.order_intents.iter().any(|intent| {
matches!(intent.unwrapped(), OrderIntent::StockPool { contract }
if crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))
});
if cumulative_conditions && (self.broker.execution_price_field() != PriceField::Close
|| start_time.is_some() || self.broker.intraday_execution_start_time().is_some()) {
let mut symbols = execution_quote_symbols_for_decision(decision, portfolio, open_orders);
self.load_missing_execution_quotes(execution_date, None, None, &mut symbols)?;
}
let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time()); let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
let post_close_window = self.broker.post_close_execution_quote_window_for_order( let post_close_window = self.broker.post_close_execution_quote_window_for_order(
execution_date, execution_date,
@@ -5796,7 +5805,7 @@ mod tests {
fn physical_on_day_rules_keep_each_actual_submission_time() { fn physical_on_day_rules_keep_each_actual_submission_time() {
let date = d(2026, 7, 6); let date = d(2026, 7, 6);
let quotes = vec![ let quotes = vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: SYMBOL.to_string(), symbol: SYMBOL.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"),
@@ -5809,7 +5818,7 @@ mod tests {
amount_delta: 110_000.0, amount_delta: 110_000.0,
trading_phase: Some("continuous_auction".to_string()), trading_phase: Some("continuous_auction".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: SYMBOL.to_string(), symbol: SYMBOL.to_string(),
timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"), timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"),
@@ -5822,7 +5831,7 @@ mod tests {
amount_delta: 990_000.0, amount_delta: 990_000.0,
trading_phase: Some("continuous_auction".to_string()), trading_phase: Some("continuous_auction".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: SYMBOL.to_string(), symbol: SYMBOL.to_string(),
timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"), timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"),
@@ -5950,7 +5959,7 @@ mod tests {
let closing_only = matches!(scenario,2|3); let closing_only = matches!(scenario,2|3);
let delayed = scenario == 4; let delayed = scenario == 4;
let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) }; let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) };
let quote = |hour, minute, price| IntradayExecutionQuote { let quote = |hour, minute, price| IntradayExecutionQuote { observation_kind: Default::default(),
date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(), date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000, last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000,
volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None, volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None,
@@ -6073,7 +6082,7 @@ mod tests {
Ok(request Ok(request
.symbols .symbols
.into_iter() .into_iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date, date: request.date,
symbol, symbol,
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"), timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
+1
View File
@@ -37,6 +37,7 @@ pub mod stock_pool_execution;
pub mod stock_pool_index_policy; pub mod stock_pool_index_policy;
pub mod stock_pool_market_cap; pub mod stock_pool_market_cap;
pub mod stock_pool_state; pub mod stock_pool_state;
pub mod stock_pool_quote_facts;
pub mod signal_contract; pub mod signal_contract;
pub mod strategy_ai; pub mod strategy_ai;
pub mod universe; pub mod universe;
+11
View File
@@ -212,6 +212,11 @@ pub fn build_dataset_context(
} }
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> { pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
// A runner bundle also contains source/extract copies. Follow the same
// authoritative spec selection as the execution loader, not those copies.
if let Some(spec) = value.get("strategySpec").or_else(|| value.get("strategy_spec")) {
return specs_in_value(spec);
}
let mut specs = Vec::new(); let mut specs = Vec::new();
match value { match value {
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?), Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
@@ -268,9 +273,15 @@ mod tests {
for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] { for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] {
let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}}); let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}});
assert_eq!(specs_in_value(&value).unwrap().len(), 2); assert_eq!(specs_in_value(&value).unwrap().len(), 2);
for wrapper in ["strategySpec", "strategy_spec"] {
let bundle = json!({wrapper:value,"strategy_source":{"source_code":source},
"strategy_extract":{"parameters":{"source_code":source}}});
assert_eq!(specs_in_value(&bundle).unwrap().len(), 2);
}
let mut invalid = value.clone(); let mut invalid = value.clone();
invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)"); invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)");
assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail"); assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail");
assert!(specs_in_value(&json!({"strategySpec":invalid})).is_err());
} }
assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1); assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1);
} }
+97 -77
View File
@@ -27,6 +27,7 @@ use crate::numeric_expr_vm::{
self, EvalError as NumericVmEvalError, Program as NumericVmProgram, self, EvalError as NumericVmEvalError, Program as NumericVmProgram,
Scratch as NumericVmScratch, Value as NumericVmValue, ValueType as NumericVmValueType, Scratch as NumericVmScratch, Value as NumericVmValue, ValueType as NumericVmValueType,
}; };
use crate::numeric_factors::NumericFactorMap;
use crate::portfolio::PortfolioState; use crate::portfolio::PortfolioState;
use crate::holding_policy::{AutomaticTradeProtection, AutomaticTradePermission, HoldingLifecycleEvidence}; use crate::holding_policy::{AutomaticTradeProtection, AutomaticTradePermission, HoldingLifecycleEvidence};
@@ -1010,7 +1011,7 @@ struct StockExpressionState {
stock_volume_ma60: f64, stock_volume_ma60: f64,
stock_volume_ma100: f64, stock_volume_ma100: f64,
current_series_end: Option<usize>, current_series_end: Option<usize>,
extra_factors: BTreeMap<String, f64>, extra_factors: NumericFactorMap,
extra_text_factors: BTreeMap<String, String>, extra_text_factors: BTreeMap<String, String>,
} }
@@ -4390,11 +4391,14 @@ impl PlatformExprStrategy {
.factor_snapshot_rows_on(date) .factor_snapshot_rows_on(date)
.iter() .iter()
.flat_map(|row| { .flat_map(|row| {
row.extra_factors.keys().map(|key| key.to_string()).chain( row.extra_factors.keys().map(|key| key.as_ref()).chain(
row.adjustment_factor_backward1 row.adjustment_factor_backward1
.map(|_| BACKWARD_ADJUSTMENT_FACTOR_FIELD.to_string()), .map(|_| BACKWARD_ADJUSTMENT_FACTOR_FIELD),
) )
}) })
.collect::<BTreeSet<_>>()
.into_iter()
.map(str::to_owned)
.collect() .collect()
} else { } else {
BTreeSet::new() BTreeSet::new()
@@ -4403,7 +4407,10 @@ impl PlatformExprStrategy {
ctx.data ctx.data
.factor_text_rows_on(date) .factor_text_rows_on(date)
.iter() .iter()
.map(|row| row.field.clone()) .map(|row| row.field.as_str())
.collect::<BTreeSet<_>>()
.into_iter()
.map(str::to_owned)
.collect() .collect()
} else { } else {
BTreeSet::new() BTreeSet::new()
@@ -4896,10 +4903,10 @@ impl PlatformExprStrategy {
self.stock_extra_factor_map_required self.stock_extra_factor_map_required
|| self.stock_extra_factor_identifiers.contains(field.as_ref()) || self.stock_extra_factor_identifiers.contains(field.as_ref())
}) })
.map(|(field, value)| (field.to_string(), *value)) .map(|(field, value)| (field.clone(), *value))
.collect() .collect()
} else { } else {
BTreeMap::new() NumericFactorMap::new()
}; };
if !self.config.completed_session_factor_fields.is_empty() { if !self.config.completed_session_factor_fields.is_empty() {
let visible_date = completed_session_factor_date( let visible_date = completed_session_factor_date(
@@ -4914,7 +4921,7 @@ impl PlatformExprStrategy {
.and_then(|row| row.extra_factors.get(field.as_str())) .and_then(|row| row.extra_factors.get(field.as_str()))
.copied() .copied()
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
extra_factors.insert(field.clone(), value); extra_factors.insert(field.clone().into(), value);
} }
} }
} }
@@ -4925,7 +4932,7 @@ impl PlatformExprStrategy {
.contains(BACKWARD_ADJUSTMENT_FACTOR_FIELD)) .contains(BACKWARD_ADJUSTMENT_FACTOR_FIELD))
&& let Some(value) = factor.adjustment_factor_backward1 && let Some(value) = factor.adjustment_factor_backward1
{ {
extra_factors.insert(BACKWARD_ADJUSTMENT_FACTOR_FIELD.to_string(), value); extra_factors.insert(BACKWARD_ADJUSTMENT_FACTOR_FIELD.into(), value);
} }
let state = StockExpressionState { let state = StockExpressionState {
@@ -5642,7 +5649,7 @@ impl PlatformExprStrategy {
Dynamic::from(stock.stock_volume_ma100), Dynamic::from(stock.stock_volume_ma100),
); );
for (key, value) in &stock.extra_factors { for (key, value) in &stock.extra_factors {
factors.insert(key.clone().into(), Dynamic::from(*value)); factors.insert(key.as_ref().into(), Dynamic::from(*value));
} }
for (key, value) in &stock.extra_text_factors { for (key, value) in &stock.extra_text_factors {
factors.insert(key.clone().into(), Dynamic::from(value.clone())); factors.insert(key.clone().into(), Dynamic::from(value.clone()));
@@ -14546,6 +14553,7 @@ mod tests {
use std::sync::Arc; use std::sync::Arc;
use chrono::{NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveTime};
use rhai::{Dynamic, Map};
use super::{ use super::{
CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage, CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage,
@@ -14718,7 +14726,7 @@ mod tests {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);
let symbol = "000001.SZ"; let symbol = "000001.SZ";
let parts = single_symbol_platform_data(&[date], symbol).snapshot_components(); let parts = single_symbol_platform_data(&[date], symbol).snapshot_components();
let quotes = [(10, 18, 9.5), (14, 59, 10.5)].into_iter().map(|(hour, minute, price)| IntradayExecutionQuote { let quotes = [(10, 18, 9.5), (14, 59, 10.5)].into_iter().map(|(hour, minute, price)| IntradayExecutionQuote { observation_kind: Default::default(),
date, symbol: symbol.to_string(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(), date, symbol: symbol.to_string(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
last_price: price, bid1: price, ask1: price, bid1_volume: 1000, ask1_volume: 1000, last_price: price, bid1: price, ask1: price, bid1_volume: 1000, ask1_volume: 1000,
volume_delta: 1000, amount_delta: price * 1000.0, trading_phase: Some("continuous".to_string()), volume_delta: 1000, amount_delta: price * 1000.0, trading_phase: Some("continuous".to_string()),
@@ -17542,6 +17550,18 @@ mod tests {
.stock_state_with_factor_date(&ctx, date, date, present_symbol) .stock_state_with_factor_date(&ctx, date, date, present_symbol)
.expect("factor map stock state"); .expect("factor map stock state");
assert!(map_stock.extra_factors.contains_key("unused_factor")); assert!(map_stock.extra_factors.contains_key("unused_factor"));
let mut numeric_state = (*map_stock).clone();
numeric_state.extra_factors.insert("negative_zero".into(), -0.0);
numeric_state.extra_factors.insert("undefined_value".into(), f64::NAN);
let copied_state = numeric_state.clone();
assert_eq!(copied_state.extra_factors["negative_zero"].to_bits(), (-0.0_f64).to_bits());
assert!(copied_state.extra_factors["undefined_value"].is_nan());
let exposed = copied_state.extra_factors.iter()
.map(|(key, value)| (key.as_ref().into(), Dynamic::from(*value)))
.collect::<Map>();
assert_eq!(exposed["negative_zero"].as_float().unwrap().to_bits(), (-0.0_f64).to_bits());
assert!(exposed["undefined_value"].as_float().unwrap().is_nan());
assert!(!exposed.contains_key("missing_factor"));
let map_day = map_strategy let map_day = map_strategy
.day_state(&ctx, date) .day_state(&ctx, date)
.expect("factor map day state"); .expect("factor map day state");
@@ -17846,7 +17866,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -17989,7 +18009,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -18077,7 +18097,7 @@ mod tests {
lower_limit: 4.50, lower_limit: 4.50,
price_tick: 0.01, price_tick: 0.01,
}; };
let quote = IntradayExecutionQuote { let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -18209,7 +18229,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -18454,7 +18474,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 39, 59).unwrap(), timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
@@ -18871,7 +18891,7 @@ mod tests {
], ],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: first_date, date: first_date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: first_date.and_hms_opt(10, 40, 0).expect("valid timestamp"), timestamp: first_date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
@@ -18884,7 +18904,7 @@ mod tests {
amount_delta: 23_990.0, amount_delta: 23_990.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: second_date, date: second_date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: second_date.and_hms_opt(10, 31, 0).expect("valid timestamp"), timestamp: second_date.and_hms_opt(10, 31, 0).expect("valid timestamp"),
@@ -19149,7 +19169,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -19162,7 +19182,7 @@ mod tests {
amount_delta: 146_200.0, amount_delta: 146_200.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -19175,7 +19195,7 @@ mod tests {
amount_delta: 145_000.0, amount_delta: 145_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: other_symbol.to_string(), symbol: other_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -19450,7 +19470,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -19463,7 +19483,7 @@ mod tests {
amount_delta: 146_300.0, amount_delta: 146_300.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: other_symbol.to_string(), symbol: other_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -19699,7 +19719,7 @@ mod tests {
], ],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: first_date, date: first_date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: first_date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: first_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -19712,7 +19732,7 @@ mod tests {
amount_delta: 56_450.0, amount_delta: 56_450.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: first_date, date: first_date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: first_date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: first_date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -19725,7 +19745,7 @@ mod tests {
amount_delta: 49_300.0, amount_delta: 49_300.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: second_date, date: second_date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: second_date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: second_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -20027,7 +20047,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -20485,7 +20505,7 @@ mod tests {
vec![candidate], vec![candidate],
vec![benchmark], vec![benchmark],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 30, 0).expect("timestamp"), timestamp: date.and_hms_opt(9, 30, 0).expect("timestamp"),
@@ -20672,7 +20692,7 @@ mod tests {
prev_close: 998.0, prev_close: 998.0,
volume: 1_000_000, volume: 1_000_000,
}; };
let quote = |date: NaiveDate, last_price: f64, volume_delta: u64| IntradayExecutionQuote { let quote = |date: NaiveDate, last_price: f64, volume_delta: u64| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
@@ -22285,7 +22305,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -22936,7 +22956,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -23090,7 +23110,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
@@ -23103,7 +23123,7 @@ mod tests {
amount_delta: 110_000.0, amount_delta: 110_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -23247,7 +23267,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
@@ -23260,7 +23280,7 @@ mod tests {
amount_delta: 108_000.0, amount_delta: 108_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -23404,7 +23424,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
@@ -23417,7 +23437,7 @@ mod tests {
amount_delta: 110_000.0, amount_delta: 110_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -23574,7 +23594,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
@@ -23889,7 +23909,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -24045,7 +24065,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
@@ -24214,7 +24234,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
@@ -24372,7 +24392,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
@@ -24494,7 +24514,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
@@ -24507,7 +24527,7 @@ mod tests {
amount_delta: 1_000.0, amount_delta: 1_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(14, 58, 59).expect("valid timestamp"), timestamp: date.and_hms_opt(14, 58, 59).expect("valid timestamp"),
@@ -24520,7 +24540,7 @@ mod tests {
amount_delta: 2_000.0, amount_delta: 2_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(14, 59, 2).expect("valid timestamp"), timestamp: date.and_hms_opt(14, 59, 2).expect("valid timestamp"),
@@ -24743,7 +24763,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 39, 59).unwrap(), timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
@@ -24980,7 +25000,7 @@ mod tests {
.collect(), .collect(),
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: execution_date, date: execution_date,
symbol: limit_symbol.to_string(), symbol: limit_symbol.to_string(),
timestamp: execution_date timestamp: execution_date
@@ -24995,7 +25015,7 @@ mod tests {
amount_delta: 233.0, amount_delta: 233.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: execution_date, date: execution_date,
symbol: fallback_symbol.to_string(), symbol: fallback_symbol.to_string(),
timestamp: execution_date timestamp: execution_date
@@ -25233,7 +25253,7 @@ mod tests {
}) })
.collect(), .collect(),
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date: execution_date, date: execution_date,
symbol: candidate_symbol.to_string(), symbol: candidate_symbol.to_string(),
timestamp: execution_date.and_hms_opt(9, 33, 0).unwrap(), timestamp: execution_date.and_hms_opt(9, 33, 0).unwrap(),
@@ -27761,7 +27781,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -28084,7 +28104,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -28242,7 +28262,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -28572,7 +28592,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"), timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
@@ -29342,7 +29362,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
@@ -29525,7 +29545,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -29736,7 +29756,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -29987,7 +30007,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30203,7 +30223,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30569,7 +30589,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30750,7 +30770,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -30763,7 +30783,7 @@ mod tests {
amount_delta: 105_000.0, amount_delta: 105_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30776,7 +30796,7 @@ mod tests {
amount_delta: 90_000.0, amount_delta: 90_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: held_symbol.to_string(), symbol: held_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30789,7 +30809,7 @@ mod tests {
amount_delta: 100_000.0, amount_delta: 100_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: buy_symbol.to_string(), symbol: buy_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -30978,7 +30998,7 @@ mod tests {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: delayed_symbol.to_string(), symbol: delayed_symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -30991,7 +31011,7 @@ mod tests {
amount_delta: 4_200.0, amount_delta: 4_200.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: held_symbol.to_string(), symbol: held_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -31004,7 +31024,7 @@ mod tests {
amount_delta: 100_000.0, amount_delta: 100_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: buy_symbol.to_string(), symbol: buy_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -31215,7 +31235,7 @@ mod tests {
.flat_map(|symbol| { .flat_map(|symbol| {
let mut quotes = Vec::new(); let mut quotes = Vec::new();
if *symbol == delayed_symbol { if *symbol == delayed_symbol {
quotes.push(IntradayExecutionQuote { quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -31229,7 +31249,7 @@ mod tests {
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}); });
} }
quotes.push(IntradayExecutionQuote { quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -31455,7 +31475,7 @@ mod tests {
.flat_map(|symbol| { .flat_map(|symbol| {
let mut quotes = Vec::new(); let mut quotes = Vec::new();
if *symbol == delayed_symbol { if *symbol == delayed_symbol {
quotes.push(IntradayExecutionQuote { quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
@@ -31469,7 +31489,7 @@ mod tests {
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}); });
} }
quotes.push(IntradayExecutionQuote { quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -32141,7 +32161,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
@@ -32430,7 +32450,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -32641,7 +32661,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.clone(), symbol: symbol.clone(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -32848,7 +32868,7 @@ mod tests {
Vec::new(), Vec::new(),
symbols symbols
.iter() .iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: (*symbol).to_string(), symbol: (*symbol).to_string(),
timestamp: date.and_hms_opt(14, 59, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(14, 59, 0).expect("valid timestamp"),
@@ -32991,7 +33011,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
@@ -33214,7 +33234,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"), timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
@@ -33351,7 +33371,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: other_symbol.to_string(), symbol: other_symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -33492,7 +33512,7 @@ mod tests {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date: decision_date, date: decision_date,
symbol: other_symbol.to_string(), symbol: other_symbol.to_string(),
timestamp: decision_date.and_hms_opt(10, 18, 0).unwrap(), timestamp: decision_date.and_hms_opt(10, 18, 0).unwrap(),
@@ -0,0 +1,148 @@
//! Condition facts are distinct from the quote's per-observation fill capacity.
//! Only a complete, declared raw-minute prefix can prove a session total.
use std::collections::BTreeMap;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use rust_decimal::Decimal;
use crate::data::IntradayExecutionQuote;
use crate::stock_pool_execution::{parse_stock_pool_condition, StockPoolExecutionRule};
pub fn requires_session_totals(rule: &StockPoolExecutionRule) -> bool {
[rule.buy_condition.as_str(), if rule.sell_trigger_mode == "condition" { rule.sell_condition.as_str() } else { "" }].into_iter().any(|condition| {
parse_stock_pool_condition(condition).is_some_and(|(_, field, _, _)| matches!(field.as_str(), "volume" | "amount"))
})
}
/// The cash-equity minute feed includes the opening observation and a separate
/// post-close segment. Trading eligibility remains owned by the dated rules.
fn next_minute(time: NaiveTime) -> Option<NaiveTime> {
let minute = time.hour() * 60 + time.minute();
let next = match minute {
570..=689 | 781..=899 | 906..=929 => minute + 1,
690 => 781,
900 => 906,
_ => return None,
};
NaiveTime::from_hms_opt(next / 60, next % 60, 0)
}
#[derive(Debug, Default)]
pub(crate) struct SessionTotalsCache {
pub date: Option<NaiveDate>,
pub symbols: BTreeMap<String, MinutePrefix>,
}
#[derive(Debug)]
pub(crate) struct MinutePrefix {
values: BTreeMap<NaiveTime, (Decimal, Decimal)>,
failure: String,
}
impl MinutePrefix {
pub fn build(date: NaiveDate, symbol: &str, quotes: &[IntradayExecutionQuote]) -> Self {
let mut values = BTreeMap::new();
let mut expected = NaiveTime::from_hms_opt(9, 30, 0).unwrap();
let mut volume = 0_u64;
let mut amount = Decimal::ZERO;
let mut failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
for quote in quotes {
let time = quote.timestamp.time();
if quote.date != date || quote.timestamp.date() != date || quote.symbol != symbol {
failure = format!("stock_pool_session_prefix_identity_invalid:{symbol}:{date}");
break;
}
if time != expected {
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:expected={expected}:observed={time}");
break;
}
if quote.observation_kind != crate::data::QuoteObservationKind::MinuteBar {
failure = format!("stock_pool_session_prefix_basis_unverified:{symbol}:{date}:{time}");
break;
}
let Some(next_volume) = volume.checked_add(quote.volume_delta) else {
failure = format!("stock_pool_session_volume_overflow:{symbol}:{date}:{time}");
break;
};
let delta = if quote.amount_delta.is_finite() && quote.amount_delta >= 0.0 {
quote.amount_delta.to_string().parse::<Decimal>().ok()
} else { None };
let Some(next_amount) = delta.and_then(|delta| amount.checked_add(delta)) else {
failure = format!("stock_pool_session_amount_invalid:{symbol}:{date}:{time}");
break;
};
volume = next_volume;
amount = next_amount;
values.insert(time, (Decimal::from(volume), amount));
let Some(next) = next_minute(time) else { break };
expected = next;
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
}
Self { values, failure }
}
pub fn at(&self, at: NaiveDateTime) -> Result<(Decimal, Decimal), String> {
let time = at.time().with_second(0).unwrap().with_nanosecond(0).unwrap();
self.values.get(&time).copied().ok_or_else(|| self.failure.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn quote(hour: u32, minute: u32, volume: u64, amount: f64) -> IntradayExecutionQuote {
let date = NaiveDate::from_ymd_opt(2026, 9, 11).unwrap();
IntradayExecutionQuote { observation_kind: crate::data::QuoteObservationKind::MinuteBar, date, symbol: "000001.SZ".into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
last_price: 10., bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
volume_delta: volume, amount_delta: amount, trading_phase: Some("minute_execution_prices:raw-minute".into()) }
}
#[test]
fn totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts() {
let mut rows = vec![quote(9,30,100,10.01), quote(9,31,0,0.), quote(9,32,200,20.02)];
let prefix = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
assert_eq!(prefix.at(rows[1].timestamp).unwrap(), (100.into(), Decimal::new(1001,2)));
assert_eq!(prefix.at(rows[2].timestamp).unwrap(), (300.into(), Decimal::new(3003,2)));
rows[2].volume_delta = 999999;
rows[2].amount_delta = f64::NAN;
let changed = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
assert_eq!(changed.at(rows[1].timestamp).unwrap(), prefix.at(rows[1].timestamp).unwrap());
assert!(changed.at(rows[2].timestamp).unwrap_err().contains("amount_invalid"));
}
#[test]
fn sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals() {
let first = quote(9,30,100,1000.);
for rows in [vec![quote(9,31,100,1000.)], vec![first.clone(), quote(9,32,100,1000.)]] {
let prefix = MinutePrefix::build(first.date, "000001.SZ", &rows);
assert!(prefix.at(rows.last().unwrap().timestamp).unwrap_err().contains("prefix_missing"));
}
let mut unknown = first.clone(); unknown.observation_kind = Default::default();
assert!(MinutePrefix::build(first.date, "000001.SZ", &[unknown]).at(first.timestamp).unwrap_err().contains("basis_unverified"));
let rows = [quote(9,30,u64::MAX,0.), quote(9,31,1,0.)];
assert!(MinutePrefix::build(first.date, "000001.SZ", &rows).at(rows[1].timestamp).unwrap_err().contains("volume_overflow"));
}
#[test]
fn lunch_and_post_close_gaps_follow_the_minute_feed_segments() {
let mut rows = Vec::new(); let mut time = NaiveTime::from_hms_opt(9,30,0).unwrap();
loop {
rows.push(quote(time.hour(), time.minute(), 1, 0.01));
let Some(next) = next_minute(time) else { break }; time=next;
}
let prefix=MinutePrefix::build(rows[0].date,"000001.SZ",&rows);
assert_eq!(prefix.at(rows.last().unwrap().timestamp).unwrap(), (Decimal::from(rows.len()), Decimal::new(rows.len() as i64,2)));
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==12));
assert!(!rows.iter().any(|row| row.timestamp.time()==NaiveTime::from_hms_opt(13,0,0).unwrap()));
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==15 && (1..6).contains(&row.timestamp.time().minute())));
}
#[test]
#[ignore = "requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response"]
fn real_source_session_prefix_matches_observed_checkpoints() {
let path=std::env::var("FIDC_SESSION_PREFIX_SOURCE_JSON").expect("explicit Source evidence path");
let rows:Vec<IntradayExecutionQuote>=serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
let date=NaiveDate::from_ymd_opt(2026,9,8).unwrap();
assert_eq!(rows.len(),242);
let prefix=MinutePrefix::build(date,"000063.SZ",&rows);
for (hour,minute,volume,amount) in [(9,30,512700,17103672),(9,31,2296631,76576756),(9,32,2983531,99471024),(11,30,27868847,928167630),(13,1,28495518,948994890),(15,0,45625008,1518115100)] {
assert_eq!(prefix.at(date.and_hms_opt(hour,minute,0).unwrap()).unwrap(),(Decimal::from(volume),Decimal::from(amount)));
}
assert!(prefix.at(date.and_hms_opt(15,30,0).unwrap()).unwrap_err().contains("prefix_missing"),"one final aggregate is not a verified intraday prefix");
}
}
@@ -196,7 +196,7 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
Ok(request Ok(request
.symbols .symbols
.into_iter() .into_iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date, date: request.date,
symbol, symbol,
timestamp: request.date.and_time(t(10, 17, 59)), timestamp: request.date.and_time(t(10, 17, 59)),
@@ -411,7 +411,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
Ok(request Ok(request
.symbols .symbols
.into_iter() .into_iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date, date: request.date,
symbol, symbol,
timestamp: request.date.and_time(t(10, 39, 59)), timestamp: request.date.and_time(t(10, 39, 59)),
@@ -556,7 +556,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
], ],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: first, date: first,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: first.and_time(t(10, 39, 59)), timestamp: first.and_time(t(10, 39, 59)),
@@ -569,7 +569,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
amount_delta: 100_000.0, amount_delta: 100_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: second, date: second,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: second.and_time(t(10, 39, 59)), timestamp: second.and_time(t(10, 39, 59)),
@@ -826,7 +826,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
Ok(request Ok(request
.symbols .symbols
.into_iter() .into_iter()
.map(|symbol| IntradayExecutionQuote { .map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
date: request.date, date: request.date,
symbol, symbol,
timestamp: request.date.and_time(start_time) - Duration::seconds(1), timestamp: request.date.and_time(start_time) - Duration::seconds(1),
+8 -8
View File
@@ -2209,7 +2209,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0), timestamp: dt(2025, 1, 2, 10, 18, 0),
@@ -2222,7 +2222,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
amount_delta: 10_200.0, amount_delta: 10_200.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0), timestamp: dt(2025, 1, 2, 10, 18, 0),
@@ -2235,7 +2235,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
amount_delta: 20_400.0, amount_delta: 20_400.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 19, 0), timestamp: dt(2025, 1, 2, 10, 19, 0),
@@ -2341,7 +2341,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);
let mut data = single_day_anchor_data(date); let mut data = single_day_anchor_data(date);
data.add_execution_quotes(vec![ data.add_execution_quotes(vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0), timestamp: dt(2025, 1, 2, 10, 18, 0),
@@ -2354,7 +2354,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
amount_delta: 10_200.0, amount_delta: 10_200.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 19, 0), timestamp: dt(2025, 1, 2, 10, 19, 0),
@@ -2519,7 +2519,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let quotes = vec![ let quotes = vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: date2, date: date2,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 3, 14, 30, 0), timestamp: dt(2025, 1, 3, 14, 30, 0),
@@ -2532,7 +2532,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
amount_delta: 10_150.0, amount_delta: 10_150.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: date3, date: date3,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 6, 10, 18, 0), timestamp: dt(2025, 1, 6, 10, 18, 0),
@@ -2545,7 +2545,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
amount_delta: 10_250.0, amount_delta: 10_250.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date: date3, date: date3,
symbol: "000001.SZ".to_string(), symbol: "000001.SZ".to_string(),
timestamp: dt(2025, 1, 6, 10, 19, 0), timestamp: dt(2025, 1, 6, 10, 19, 0),
+25 -25
View File
@@ -146,7 +146,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -159,7 +159,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
amount_delta: 10_000.0, amount_delta: 10_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 19, 0).unwrap(), timestamp: date.and_hms_opt(10, 19, 0).unwrap(),
@@ -172,7 +172,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
amount_delta: 10_000.0, amount_delta: 10_000.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(10, 20, 0).unwrap(), timestamp: date.and_hms_opt(10, 20, 0).unwrap(),
@@ -373,7 +373,7 @@ fn broker_executes_explicit_order_value_buy() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -527,7 +527,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
timestamp: date.and_hms_opt(9, 31, 0).unwrap(), timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
@@ -663,7 +663,7 @@ fn broker_executes_order_shares_and_order_lots() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -1104,7 +1104,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(9, 31, 0).unwrap(), timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
@@ -1117,7 +1117,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(9, 35, 0).unwrap(), timestamp: date.and_hms_opt(9, 35, 0).unwrap(),
@@ -1920,7 +1920,7 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -2153,7 +2153,7 @@ fn broker_executes_intraday_last_on_start_quote_with_trade_delta() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(9, 33, 0).unwrap(), timestamp: date.and_hms_opt(9, 33, 0).unwrap(),
@@ -2273,7 +2273,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -2509,7 +2509,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -2522,7 +2522,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 6).unwrap(), timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
@@ -2682,7 +2682,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -2695,7 +2695,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 6).unwrap(), timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
@@ -2839,7 +2839,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 17, 59).unwrap(), timestamp: date.and_hms_opt(10, 17, 59).unwrap(),
@@ -2852,7 +2852,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -2865,7 +2865,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 6).unwrap(), timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
@@ -2878,7 +2878,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 40).unwrap(), timestamp: date.and_hms_opt(10, 18, 40).unwrap(),
@@ -3001,7 +3001,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
}], }],
Vec::new(), Vec::new(),
vec![ vec![
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 0, 0).unwrap(), timestamp: date.and_hms_opt(10, 0, 0).unwrap(),
@@ -3014,7 +3014,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 15, 0).unwrap(), timestamp: date.and_hms_opt(10, 15, 0).unwrap(),
@@ -3027,7 +3027,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
amount_delta: 0.0, amount_delta: 0.0,
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}, },
IntradayExecutionQuote { IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 30, 0).unwrap(), timestamp: date.and_hms_opt(10, 30, 0).unwrap(),
@@ -3165,7 +3165,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -3284,7 +3284,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
volume: 1_000_000, volume: 1_000_000,
}], }],
Vec::new(), Vec::new(),
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote { observation_kind: Default::default(),
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
@@ -4915,7 +4915,7 @@ fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
}, },
], ],
Vec::new(), Vec::new(),
[(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote { [(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote { observation_kind: Default::default(),
date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(), date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(),
last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0, last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0,
volume_delta: 100_000, amount_delta: 100_000.0 * price, volume_delta: 100_000, amount_delta: 100_000.0 * price,
@@ -55,7 +55,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start"); let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
for offset in 0..bars_per_day { for offset in 0..bars_per_day {
let timestamp = session_start + Duration::minutes(offset as i64); let timestamp = session_start + Duration::minutes(offset as i64);
quotes.push(IntradayExecutionQuote { quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date: *date, date: *date,
symbol: SYMBOL.to_string(), symbol: SYMBOL.to_string(),
timestamp, timestamp,
@@ -7,6 +7,7 @@ use fidc_core::{
PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value, PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value,
}; };
use rust_decimal::Decimal; use rust_decimal::Decimal;
use fidc_core::IntradayExecutionQuote;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
fn day(n: u32) -> NaiveDate { fn day(n: u32) -> NaiveDate {
@@ -142,7 +143,7 @@ fn data_with_fund_rules(
}) })
}) })
.collect(); .collect();
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote { let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote { observation_kind: Default::default(),
date: row.date, symbol: row.symbol.to_string(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(), date: row.date, symbol: row.symbol.to_string(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0, last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
volume_delta: row.volume, amount_delta: row.open * row.volume as f64, volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
@@ -489,6 +490,72 @@ fn repeating_the_same_partial_exit_generation_does_not_reduce_again() {
assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300); assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300);
} }
#[test]
fn daily_execution_price_does_not_satisfy_an_unobserved_order_book_condition() {
let data = data(false);
for field in ["bid1", "ask1"] {
let broker = broker(false);
let mut account = PortfolioState::new(30000.);
let mut intent = contract(day(2), 1, false);
intent.rule.trigger_mode = "condition".into();
intent.rule.buy_condition = format!("{field}>0");
let result = broker.execute_with_event_dates(day(5), day(2), day(2), &mut account, &data, &decision(intent));
assert!(result.unwrap_err().to_string().contains(field));
assert!(account.positions().is_empty());
assert_eq!(account.cash(), 30000.);
}
}
#[test]
fn cumulative_conditions_do_not_consume_future_bars_or_inflate_fill_capacity() {
let mut data = data(false);
let mut quotes = Vec::new();
for n in 1..=2 {
let price = if n == 1 {20.} else {10.};
for (minute, volume) in [(30,600), (31,0), (32,400)] {
quotes.push(IntradayExecutionQuote {
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
date: day(5), symbol: code(n), timestamp: day(5).and_hms_opt(9,minute,0).unwrap(),
last_price: price, bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
volume_delta: volume, amount_delta: volume as f64 * price, trading_phase: None,
});
}
}
data.replace_execution_quotes(quotes.clone());
let at = chrono::NaiveTime::from_hms_opt(9,32,0).unwrap();
for condition in ["volume>=1000", "amount>=20000"] {
let broker=broker(true).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
let mut account=PortfolioState::new(30000.);
let mut intent=contract(day(5),1,false);
intent.rule.buy_condition=condition.into();intent.rule.trigger_mode="condition".into();
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
assert_eq!(report.fill_events.iter().map(|fill|fill.quantity).sum::<u32>(),100,"{condition}: {report:?}");
assert_eq!(data.execution_quotes_on(day(5),&code(1))[2].volume_delta,400);
}
let mut future=quotes.last().unwrap().clone();future.symbol=code(1);future.timestamp=day(5).and_hms_opt(9,33,0).unwrap();future.volume_delta=9000;future.amount_delta=180000.;
data.add_execution_quotes(vec![future]);
let broker=broker(false).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
let mut account=PortfolioState::new(30000.);
let mut intent=contract(day(5),1,false);intent.rule.buy_condition="volume>1000".into();intent.rule.trigger_mode="condition".into();
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
assert!(report.fill_events.is_empty(),"future volume must not satisfy this signal: {report:?}");
}
#[test]
fn session_total_cache_is_invalidated_without_mutating_other_dataset_clones() {
let mut original=data(false);
let quote=IntradayExecutionQuote { observation_kind:fidc_core::data::QuoteObservationKind::MinuteBar,date:day(5),symbol:code(1),timestamp:day(5).and_hms_opt(9,30,0).unwrap(),last_price:20.,bid1:0.,ask1:0.,bid1_volume:0,ask1_volume:0,volume_delta:100,amount_delta:2000.,trading_phase:None };
original.replace_execution_quotes(vec![quote.clone()]);
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
let mut changed=original.clone();let mut next=quote.clone();next.timestamp=day(5).and_hms_opt(9,31,0).unwrap();
changed.add_execution_quotes(vec![next.clone()]);
assert_eq!(changed.execution_session_totals(&code(1),next.timestamp).unwrap().0,Decimal::from(200));
assert!(original.execution_session_totals(&code(1),next.timestamp).is_err());
changed.remove_execution_quotes_on_date(day(5));
assert!(changed.execution_session_totals(&code(1),quote.timestamp).is_err());
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
}
#[test] #[test]
fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() { fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() {
let intent = contract(day(2), 1, false); let intent = contract(day(2), 1, false);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
# 表达式上下文性能验收
## 范围
本轮优化 FIDC 引擎的逐股票表达式上下文,不修改策略、因子值、窗口、时间可见性、
选股/订单规则、费用、成交价、风控或公司行为。Source 保持 `d5b682c6d097`
研究和信号保持暂停。其他用户任务只读观察,不更改其进程、亲和性或配置。
已完成编译、回归、正式回放与177发布验收,不能据此关闭整个目标。
## 重复开销
1. DataSet 已采用 `NumericFactorMap`,但 `StockExpressionState` 仍把数值因子
重建为 `BTreeMap<String, f64>`,增加树节点和字符串分配。
2. 每日可用因子名集合先为每个证券复制名称,再由集合丢弃重复名称。
候选在表达式上下文延续紧凑数值存储。每日名称仍按当日真实字段生成完整集合,
仅改为先对借用名称去重,再为唯一名称分配字符串;文本因子同理。
没有用全局/未来日期目录替代当日字段,没有缓存选股结果或账户状态。
Rhai `factors[...]`、缺失、NaN、负零、别名、覆盖顺序与已完成交易日可见性保持原行为。
## CPU 计时
Runner 新增以下运行计时,HTTP benchmark 同样保留:
- `engineTaskWallSeconds`:实际引擎同步任务的墙钟耗时,包含其同步数据读取等待。
- `engineThreadCpuSeconds`Linux `CLOCK_THREAD_CPUTIME_ID` 实测的引擎调用线程CPU时间。
不包含其他并行线程、I/O等待或未被调度的时间;不可当作整个进程总CPU时间。
读取不可用、跨线程或时钟异常保持null,不填0。
- `engineTaskCount`:实际执行引擎的次数,包含原有修复循环的重放。
这些是 `engineSeconds` 的子指标,禁止再次加到总耗时。正常耗时与诊断运行分开保存。
计时不改写历史制品,旧记录缺少该指标时保持未知。
## 验收设置
- 固定引擎基线 `fe7243b`;候选为177的 `07b7b18`,对应本地 `df1862e`
- 两边使用同一计时版service `a9df11a``b5d22ff`仅补充benchmark字段读取。
- 2021-08-23至2025-11-17、初始1000万、原冻结runtime与策略,1025个执行交易日。
- 保留原 `session_capacity_audit`,不能当作实际开盘流动性验证。
- 官方benchmark入口、Boris执行、同CPU资源与Source版本、新进程、相同数据缓存副本、
新结果目录,不复用回测结果。
- 引擎780项、runner408项、API113项、脚本10项通过;9/8/3项手动或外部环境用例分别忽略。
- 专项延伸验证紧凑因子的克隆、Rhai映射暴露、缺失、NaN及负零;CPU计时验证睡眠和跨线程边界。
证据根:`/srv/fidc/canonical/run/research/engine-context-20260913`
## 独立进程对照
| 次序 | 样本 | 完整墙钟 | Source校验 | 数据准备 | 引擎墙钟 | 引擎线程CPU |
|---|---|---:|---:|---:|---:|---:|
| 1 | control-1 | 31.234s | 11.202s | 8.393s | 10.461s | 10.458s |
| 2 | candidate-1 | 17.002s | 0.004s | 8.404s | 7.406s | 7.404s |
| 3 | candidate-2 | 18.203s | 0.003s | 8.371s | 7.411s | 7.408s |
| 4 | control-2 | 22.983s | 0.004s | 8.323s | 13.401s | 13.398s |
| 5 | control-3 | 30.714s | 0.005s | 15.318s | 13.999s | 13.990s |
| 6 | candidate-3 | 25.336s | 0.005s | 13.524s | 10.471s | 10.468s |
首个基线的Source校验等待原样保留,不事后改称预热,不把11.202秒归因于引擎改动。
后段样本出现主机负载/缓存竞争变化,数据准备也变慢,不能直接用全组平均墙钟夸大提速。
相邻低负载对照的引擎线程CPU为10.458至7.404秒,后段为13.990至10.468秒。
CPU计时与任务墙钟非常接近,证明样本主要在执行CPU工作,而不是等待HTTP;
这不代表没有SMT、内存带宽或其他用户CPU竞争。
六次均为21,393笔成交,账户、权益、委托、成交、持仓和风控canonical及结果制品完全一致。
每份63个数据缓存文件经完整SHA核对相同,没有复制或读取旧回测结果。
## HTTP 对照
| 状态 | 版本 | 运行ID | 总耗时 | 引擎耗时 |
|---|---|---|---:|---:|
| 清DataSet,磁盘/Source保持 | 原版 | btr_1789232559582_3166774_4 | 21.987s | 11.328s |
| 清DataSet,磁盘/Source保持 | 原版 | btr_1789232585690_3166774_5 | 21.684s | 11.259s |
| 复用DataSet | 原版 | btr_1789232669598_3166774_6 | 11.820s | 11.031s |
| 复用DataSet | 原版 | btr_1789232684861_3166774_7 | 11.857s | 11.067s |
| 清DataSet,磁盘/Source保持 | 新版 | btr_1789232818009_3320588_0 | 17.296s | 7.537s |
| 清DataSet,磁盘/Source保持 | 新版 | btr_1789232839269_3320588_1 | 17.413s | 7.627s |
| 复用DataSet | 新版 | btr_1789232898983_3320588_2 | 8.549s | 7.738s |
| 复用DataSet | 新版 | btr_1789232910904_3320588_3 | 8.586s | 7.784s |
同状态HTTP均值:重建DataSet从21.836至17.355秒,减少约20.5%
复用DataSet从11.839至8.568秒,减少约27.6%。两种状态分开比较,
没有把8.568秒当作Source冷启动成绩。与上一轮不同时间的15/17秒样本不作直接百分比对比。
原版API没有线程CPU字段,保持null;新版本每次实际执行引擎一次,
两次重建的线程CPU为7.535/7.624秒。没有用新版本计时回填旧记录。
八次HTTP和六次独立回放的canonical及结果制品SHA全部相同,终态审计clean。
## 发布状态
177通过官方安装器发布 engine `07b7b181b60138c6ef1c965543c0e3192ac65903`
service `b5d22ffab16f851eced3028e12fa02627ee4c399`
运行身份 `fdd8652a47a5935be4d891beb3b8b0f3e19a468be166a902a2a97b85a9c9e01e`
- API SHA`bf22f58946c3fa495161eb381a400d4e28d7c8d327ee46f5645d83a8308117cf`
- Runner SHA`7b3849cd8af33d650db242add80c49cfdd32e8cc8686a614da7b3b4016ce2a60`
- 生产在用构建根:`/srv/fidc/canonical/build/engine-context-candidate-20260913`,禁止清理。
- 原生因子能力目录发布前后字节相同,SHA为
`cec37331a476bc39bdea32c308581b8ac2f86d005d8dd4cc7ba228c5d9dc9a2e`
- API PID3320588Boris、active、NRestarts=0Source仍为PID1700096/d5,研究未恢复。
[完整结构化验收证据](evidence/expression-context-performance-20260913.json)
SHA256 `f526950e018354c1305922beebf4063ae3823004f8c5ab20510a452f98b7b7ea`
## 边界
本轮真实长区间案例含一个原生扩展因子,动态映射、缺失及多字段语义另由引擎回归覆盖;
不宣称所有策略都具有相同比例提速。Source冷路径仍受独立冻结约束,
信号闭环和全部策略/分钟区间/财务PIT不在本轮通过范围内。