复用选股日快照视图

This commit is contained in:
boris
2026-08-31 05:03:20 +08:00
parent b634540047
commit cb18a3f279
3 changed files with 280 additions and 4 deletions
+223
View File
@@ -1335,6 +1335,55 @@ pub struct DataSet {
futures_params_by_symbol: Arc<HashMap<String, Vec<FuturesTradingParameter>>>,
}
struct DailySymbolRows<'a, T> {
rows: &'a [T],
symbol_ids: &'a [u32],
row_positions: Option<&'a [u32]>,
}
impl<'a, T> DailySymbolRows<'a, T> {
fn get(&self, symbol_id: u32) -> Option<&'a T> {
if let Some(positions) = self.row_positions {
let position = positions.get(symbol_id as usize).copied()?;
if position == MISSING_ROW_POSITION {
return None;
}
return self.rows.get(position as usize);
}
find_by_symbol_id(self.rows, self.symbol_ids, symbol_id)
}
}
/// Borrowed, immutable snapshots for one trading date.
///
/// A strategy evaluates thousands of symbols for the same date. Resolving the
/// date in three BTreeMaps for every symbol is unnecessary; this view freezes
/// the already indexed slices once and keeps all lookups read-only.
pub(crate) struct DailySnapshotView<'a> {
market: DailySymbolRows<'a, DailyMarketSnapshot>,
factor_rows: &'a [DailyFactorSnapshot],
factor_symbol_ids: &'a [u32],
candidates: DailySymbolRows<'a, CandidateEligibility>,
}
impl<'a> DailySnapshotView<'a> {
pub(crate) fn market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot> {
self.market.get(symbol_id)
}
pub(crate) fn candidate(&self, symbol_id: u32) -> Option<&'a CandidateEligibility> {
self.candidates.get(symbol_id)
}
pub(crate) fn factor_rows(&self) -> &'a [DailyFactorSnapshot] {
self.factor_rows
}
pub(crate) fn factor_symbol_ids(&self) -> &'a [u32] {
self.factor_symbol_ids
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct StandardRollingMeans {
pub close: [Option<f64>; 7],
@@ -1868,6 +1917,52 @@ impl DataSet {
)
}
pub(crate) fn daily_snapshot_view(&self, date: NaiveDate) -> DailySnapshotView<'_> {
fn rows_on<'a, T>(
date: NaiveDate,
rows_by_date: &'a BTreeMap<NaiveDate, Vec<T>>,
symbol_ids_by_date: &'a BTreeMap<NaiveDate, Vec<u32>>,
row_positions_by_date: &'a Option<DenseRowPositionIndex>,
) -> DailySymbolRows<'a, T> {
DailySymbolRows {
rows: rows_by_date.get(&date).map(Vec::as_slice).unwrap_or(&[]),
symbol_ids: symbol_ids_by_date
.get(&date)
.map(Vec::as_slice)
.unwrap_or(&[]),
row_positions: row_positions_by_date
.as_ref()
.and_then(|positions| positions.get(&date))
.map(Vec::as_slice),
}
}
DailySnapshotView {
market: rows_on(
date,
&self.market_by_date,
&self.market_symbol_ids_by_date,
&self.market_row_positions_by_date,
),
factor_rows: self
.factor_by_date
.get(&date)
.map(Vec::as_slice)
.unwrap_or(&[]),
factor_symbol_ids: self
.factor_symbol_ids_by_date
.get(&date)
.map(Vec::as_slice)
.unwrap_or(&[]),
candidates: rows_on(
date,
&self.candidate_by_date,
&self.candidate_symbol_ids_by_date,
&self.candidate_row_positions_by_date,
),
}
}
fn market_series(&self, symbol: &str) -> Option<&SymbolPriceSeries> {
self.market_series_by_symbol.get(symbol).map(Arc::as_ref)
}
@@ -4775,11 +4870,16 @@ mod tests {
for symbol in ["000001.SZ", "600000.SH"] {
let symbol_id = data.symbol_id(symbol).unwrap();
let day = data.daily_snapshot_view(date);
assert_eq!(
data.market_by_symbol_id(date, symbol_id)
.map(|row| row.symbol.as_str()),
Some(symbol)
);
assert_eq!(
day.market(symbol_id).map(|row| row.symbol.as_str()),
Some(symbol)
);
assert_eq!(
data.factor_by_symbol_id(date, symbol_id)
.map(|row| row.symbol.as_str()),
@@ -4790,15 +4890,138 @@ mod tests {
.map(|row| row.symbol.as_str()),
Some(symbol)
);
assert_eq!(
day.candidate(symbol_id).map(|row| row.symbol.as_str()),
Some(symbol)
);
}
let signal_id = data.symbol_id("000300.SH").unwrap();
let day = data.daily_snapshot_view(date);
assert_eq!(
data.market_by_symbol_id(date, signal_id).map(|row| row.symbol.as_str()),
Some("000300.SH")
);
assert!(data.factor_by_symbol_id(date, signal_id).is_none());
assert!(data.candidate_by_symbol_id(date, signal_id).is_none());
assert!(day.candidate(signal_id).is_none());
}
#[test]
#[ignore = "manual component benchmark"]
fn benchmark_daily_snapshot_view_lookup() {
use std::hint::black_box;
use std::time::Instant;
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
let symbol_count = 6_000usize;
let symbols = (0..symbol_count)
.map(|index| format!("{index:06}.SZ"))
.collect::<Vec<_>>();
let instruments = symbols
.iter()
.map(|symbol| Instrument {
symbol: symbol.clone(),
name: symbol.clone(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: None,
delisted_at: None,
status: "active".to_string(),
})
.collect::<Vec<_>>();
let market = symbols
.iter()
.enumerate()
.map(|(index, symbol)| {
let mut row = market_row(
"2025-01-02",
10.0 + index as f64 / 1000.0,
1_000_000,
);
row.symbol = symbol.clone();
row
})
.collect::<Vec<_>>();
let factors = symbols
.iter()
.enumerate()
.map(|(index, symbol)| DailyFactorSnapshot {
date,
symbol: symbol.clone(),
market_cap_bn: 10.0 + index as f64 / 1000.0,
free_float_cap_bn: 8.0,
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: NumericFactorMap::new(),
})
.collect::<Vec<_>>();
let candidates = symbols
.iter()
.map(|symbol| CandidateEligibility {
date,
symbol: symbol.clone(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: None,
})
.collect::<Vec<_>>();
let data = DataSet::from_components(
instruments,
market,
factors,
candidates,
vec![benchmark_row("2025-01-02", 20.0)],
)
.unwrap();
let symbol_ids = symbols
.iter()
.map(|symbol| data.symbol_id(symbol).unwrap())
.collect::<Vec<_>>();
let rounds = 200usize;
let started = Instant::now();
let mut baseline_sum = 0.0;
for _ in 0..rounds {
for symbol_id in symbol_ids.iter().copied() {
baseline_sum += black_box(
data.market_by_symbol_id(date, symbol_id).unwrap().close
+ data
.candidate_by_symbol_id(date, symbol_id)
.unwrap()
.allow_buy as u8 as f64,
);
}
}
let baseline = started.elapsed();
let day = data.daily_snapshot_view(date);
let started = Instant::now();
let mut view_sum = 0.0;
for _ in 0..rounds {
for symbol_id in symbol_ids.iter().copied() {
view_sum += black_box(
day.market(symbol_id).unwrap().close
+ day.candidate(symbol_id).unwrap().allow_buy as u8 as f64,
);
}
}
let view = started.elapsed();
assert_eq!(baseline_sum, view_sum);
println!(
"daily_snapshot_view rows={} rounds={} baseline_seconds={:.6} view_seconds={:.6}",
symbol_count,
rounds,
baseline.as_secs_f64(),
view.as_secs_f64(),
);
}
#[test]
@@ -8886,8 +8886,10 @@ impl PlatformExprStrategy {
) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) {
let mut rows = Vec::new();
let mut decisions = Vec::new();
let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date);
let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_date);
let execution_day = ctx.data.daily_snapshot_view(date);
let factor_day = ctx.data.daily_snapshot_view(factor_date);
let factor_rows = factor_day.factor_rows();
let factor_symbol_ids = factor_day.factor_symbol_ids();
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
@@ -8895,14 +8897,14 @@ impl PlatformExprStrategy {
}
let synthetic_candidate;
let candidate =
if let Some(candidate) = ctx.data.candidate_by_symbol_id(date, symbol_id) {
if let Some(candidate) = execution_day.candidate(symbol_id) {
candidate
} else {
synthetic_candidate =
crate::data::missing_candidate_risk_state(date, &factor.symbol);
&synthetic_candidate
};
let Some(market) = ctx.data.market_by_symbol_id(date, symbol_id) else {
let Some(market) = execution_day.market(symbol_id) else {
continue;
};
let (reject_from_universe, selection_decision) = if collect_risk_decisions {