保持日内覆盖与按股序列一致

This commit is contained in:
boris
2026-09-07 13:25:07 +08:00
parent d5af51c02b
commit 3f6017d98b
+128 -22
View File
@@ -900,6 +900,37 @@ impl SymbolPriceSeries {
}
}
fn apply_intraday_market_overlays(
&mut self,
overlays: &[&IntradayMarketSnapshotOverlay],
) -> Result<(), NaiveDate> {
let mut last_price_changed = false;
for overlay in overlays {
let index = self
.dates
.binary_search(&overlay.date)
.map_err(|_| overlay.date)?;
self.timestamps[index] = overlay.timestamp.clone();
if let Some(last_price) = overlay
.last_price
.filter(|value| value.is_finite() && *value > 0.0)
{
self.last_prices[index] = last_price;
last_price_changed = true;
}
self.bid1s[index] = overlay.bid1;
self.ask1s[index] = overlay.ask1;
self.minute_volumes[index] = overlay.minute_volume;
self.bid1_volumes[index] = overlay.bid1_volume;
self.ask1_volumes[index] = overlay.ask1_volume;
self.trading_phases[index] = overlay.trading_phase.clone();
}
if last_price_changed {
self.last_prefix = prefix_sums(&self.last_prices);
}
Ok(())
}
fn moving_average(&self, date: NaiveDate, lookback: usize, field: PriceField) -> Option<f64> {
if lookback == 0 {
return None;
@@ -2238,7 +2269,7 @@ impl DataSet {
.sum()
}
/// Applies sparse intraday fields without rebuilding daily series or indexes.
/// Applies sparse intraday fields without rebuilding unaffected daily series or indexes.
///
/// The daily market storage must still be uniquely owned. This is deliberate:
/// silently using `Arc::make_mut` here would deep-copy the full market panel
@@ -2251,8 +2282,25 @@ impl DataSet {
return Ok(0);
}
for (component, strong_count) in [
("daily market panel", Arc::strong_count(&self.market_by_date)),
(
"market series by symbol",
Arc::strong_count(&self.market_series_by_symbol),
),
(
"market series by symbol id",
Arc::strong_count(&self.market_series_by_symbol_id),
),
] {
if strong_count != 1 {
return Err(DataSetError::SharedComponentMutation { component });
}
}
let mut resolved = Vec::with_capacity(overlays.len());
let mut seen = HashSet::<(NaiveDate, u32)>::with_capacity(overlays.len());
let mut overlay_indexes_by_symbol_id = BTreeMap::<u32, Vec<usize>>::new();
for overlay in overlays {
let symbol_id = self
.symbol_id_by_code
@@ -2270,33 +2318,71 @@ impl DataSet {
});
}
let row_position = self
.market_symbol_ids_by_date
.get(&overlay.date)
.and_then(|symbol_ids| symbol_ids.binary_search(&symbol_id).ok())
.market_row_positions_by_date
.as_ref()
.as_ref()
.and_then(|positions_by_date| positions_by_date.get(&overlay.date))
.and_then(|positions| positions.get(symbol_id as usize))
.copied()
.filter(|position| *position != MISSING_ROW_POSITION)
.map(|position| position as usize)
.or_else(|| {
self.market_symbol_ids_by_date
.get(&overlay.date)
.and_then(|symbol_ids| symbol_ids.binary_search(&symbol_id).ok())
})
.ok_or_else(|| DataSetError::MissingSnapshot {
kind: "intraday_overlay_market",
date: overlay.date,
symbol: overlay.symbol.clone(),
})?;
resolved.push((overlay.date, row_position, overlay));
let overlay_index = resolved.len();
resolved.push((overlay.date, row_position, symbol_id, overlay));
overlay_indexes_by_symbol_id
.entry(symbol_id)
.or_default()
.push(overlay_index);
}
let market_by_date = Arc::get_mut(&mut self.market_by_date).ok_or(
DataSetError::SharedComponentMutation {
component: "daily market panel",
},
)?;
for (date, row_position, overlay) in resolved {
let mut series_replacements = Vec::with_capacity(overlay_indexes_by_symbol_id.len());
for (symbol_id, overlay_indexes) in overlay_indexes_by_symbol_id {
let existing = self
.market_series_by_symbol_id
.get(symbol_id as usize)
.and_then(Option::as_ref)
.ok_or_else(|| DataSetError::MissingSnapshot {
kind: "intraday_overlay_market_series",
date: resolved[overlay_indexes[0]].0,
symbol: resolved[overlay_indexes[0]].3.symbol.clone(),
})?;
let mut updated = (**existing).clone();
let series_overlays = overlay_indexes
.iter()
.map(|index| &resolved[*index].3)
.collect::<Vec<_>>();
updated
.apply_intraday_market_overlays(&series_overlays)
.map_err(|date| DataSetError::MissingSnapshot {
kind: "intraday_overlay_market_series_date",
date,
symbol: updated.symbol.clone(),
})?;
series_replacements.push((symbol_id, Arc::new(updated)));
}
let market_by_date = Arc::get_mut(&mut self.market_by_date)
.expect("daily market panel uniqueness checked before overlay");
for (date, row_position, _, overlay) in &resolved {
let row = market_by_date
.get_mut(&date)
.and_then(|rows| rows.get_mut(row_position))
.get_mut(date)
.and_then(|rows| rows.get_mut(*row_position))
.ok_or_else(|| DataSetError::MissingSnapshot {
kind: "intraday_overlay_market_row",
date,
date: *date,
symbol: overlay.symbol.clone(),
})?;
debug_assert_eq!(row.symbol, overlay.symbol);
row.timestamp = overlay.timestamp;
row.timestamp = overlay.timestamp.clone();
if let Some(last_price) = overlay
.last_price
.filter(|value| value.is_finite() && *value > 0.0)
@@ -2308,9 +2394,19 @@ impl DataSet {
row.minute_volume = overlay.minute_volume;
row.bid1_volume = overlay.bid1_volume;
row.ask1_volume = overlay.ask1_volume;
row.trading_phase = overlay.trading_phase;
row.trading_phase = overlay.trading_phase.clone();
}
Ok(seen.len())
let market_series_by_symbol = Arc::get_mut(&mut self.market_series_by_symbol)
.expect("market series map uniqueness checked before overlay");
let market_series_by_symbol_id = Arc::get_mut(&mut self.market_series_by_symbol_id)
.expect("market series id map uniqueness checked before overlay");
for (symbol_id, series) in series_replacements {
let symbol = self.symbol_by_id[symbol_id as usize].to_string();
market_series_by_symbol.insert(symbol, Arc::clone(&series));
market_series_by_symbol_id[symbol_id as usize] = Some(series);
}
Ok(resolved.len())
}
/// Replaces the run-local execution quote layer without touching the
@@ -4966,7 +5062,7 @@ mod tests {
}
#[test]
fn unique_dataset_applies_sparse_intraday_overlay_without_rebuilding_daily_series() {
fn unique_dataset_applies_sparse_intraday_overlay_to_daily_and_symbol_views() {
let date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let mut data = DataSet::from_components(
vec![Instrument {
@@ -5016,12 +5112,22 @@ mod tests {
assert_eq!(market.ask1_volume, 3_200);
assert_eq!(market.trading_phase.as_deref(), Some("continuous"));
assert_eq!(market.close, 10.0);
assert!(Arc::ptr_eq(
let market_series_after = data.market_series_by_symbol_id
[data.symbol_id("000001.SZ").unwrap() as usize]
.as_ref()
.unwrap();
assert!(!Arc::ptr_eq(
&market_series_before,
data.market_series_by_symbol_id[data.symbol_id("000001.SZ").unwrap() as usize]
.as_ref()
.unwrap()
market_series_after
));
assert_eq!(
serde_json::to_value(market_series_after.snapshot_at(0)).unwrap(),
serde_json::to_value(market).unwrap()
);
assert_eq!(
market_series_after.moving_average(date, 1, PriceField::Last),
Some(10.08)
);
}
#[test]