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

This commit is contained in:
boris
2026-09-12 18:50:12 +08:00
parent 61bd14d001
commit 875e31f71f
12 changed files with 407 additions and 133 deletions
+30 -5
View File
@@ -284,6 +284,8 @@ pub struct CorporateAction {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntradayExecutionQuote {
#[serde(default)]
pub observation_kind: QuoteObservationKind,
#[serde(with = "date_format")]
pub date: NaiveDate,
pub symbol: String,
@@ -301,6 +303,14 @@ pub struct IntradayExecutionQuote {
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.
///
/// 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>>>,
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
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>>>,
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
@@ -1941,6 +1952,7 @@ impl DataSet {
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
corporate_actions_by_date: Arc::new(corporate_actions_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),
order_book_depth_index: Arc::new(order_book_depth_index),
benchmark_by_date: Arc::new(benchmark_by_date),
@@ -2279,6 +2291,15 @@ impl DataSet {
.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 {
self.execution_quotes_by_date
.get(&date)
@@ -2451,6 +2472,7 @@ impl DataSet {
/// Replaces the run-local execution quote layer without touching the
/// immutable daily panel.
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 quote_count = execution_quotes_by_date
.values()
@@ -2466,6 +2488,7 @@ impl DataSet {
}
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();
for quote in quotes {
grouped
@@ -2566,6 +2589,7 @@ impl DataSet {
}
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 Some(rows_by_symbol) = removed else {
return 0;
@@ -2578,6 +2602,7 @@ impl DataSet {
}
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
.execution_quotes_by_date
.get(&date)
@@ -5158,7 +5183,7 @@ mod tests {
&run_data.execution_quote_dates
));
run_data.add_execution_quotes(vec![IntradayExecutionQuote {
run_data.add_execution_quotes(vec![IntradayExecutionQuote { observation_kind: Default::default(),
date,
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
@@ -5301,7 +5326,7 @@ mod tests {
vec![benchmark_row("2025-01-02", 12.0)],
)
.unwrap();
let quote = IntradayExecutionQuote {
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date,
symbol: "000001.SZ".to_string(),
timestamp,
@@ -5403,7 +5428,7 @@ mod tests {
successor_cash: None,
};
corporate_actions.push(corporate_action.clone());
execution_quotes.push(IntradayExecutionQuote {
execution_quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
date,
symbol: symbols[0].to_string(),
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
@@ -6114,7 +6139,7 @@ mod tests {
vec![benchmark_row("2025-01-02", 12.0)],
)
.unwrap();
let quote = |symbol: &str, time: &str| IntradayExecutionQuote {
let quote = |symbol: &str, time: &str| IntradayExecutionQuote { observation_kind: Default::default(),
date,
timestamp: NaiveDateTime::parse_from_str(
&format!("2025-01-02 {time}"),
@@ -6193,7 +6218,7 @@ mod tests {
#[test]
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 quote = IntradayExecutionQuote {
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
date,
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),