复用选股日快照视图
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Market Day View Component Benchmark
|
||||
|
||||
Date: 2026-08-31
|
||||
|
||||
## Scope
|
||||
|
||||
The platform-expression selection loop already iterates one factor slice for a
|
||||
single trading date. The previous implementation still resolved the same date
|
||||
in the market and candidate `BTreeMap` for every symbol. `DailySnapshotView`
|
||||
borrows the existing immutable market/factor/candidate slices and dense row
|
||||
position arrays once per date, then performs only `symbol_id -> row` lookups.
|
||||
|
||||
The view does not copy snapshots, cache strategy results, share account state,
|
||||
or change missing-row behavior. The optimization is independent of strategy
|
||||
text, thresholds, rolling windows, execution mode and portfolio size.
|
||||
|
||||
## Release Component A/B
|
||||
|
||||
Contract:
|
||||
|
||||
- 6,000 symbols;
|
||||
- 200 complete lookup rounds;
|
||||
- each lookup reads market close and candidate `allow_buy`;
|
||||
- baseline and view checksums must be exactly equal;
|
||||
- `cargo test --release`, system allocator, local macOS host.
|
||||
|
||||
| Round | Baseline seconds | Day view seconds |
|
||||
| ---: | ---: | ---: |
|
||||
| 1 | 0.009000 | 0.002939 |
|
||||
| 2 | 0.004370 | 0.001555 |
|
||||
| 3 | 0.004274 | 0.001578 |
|
||||
|
||||
Median component time changed from `0.004370s` to `0.001578s`, an observed
|
||||
reduction of about `63.9%` (`2.77x`). This is a component result only and is
|
||||
not a complete backtest SLA.
|
||||
|
||||
## Correctness Gates
|
||||
|
||||
- sparse market-only symbols remain absent from factor/candidate views;
|
||||
- dense and binary-search fallback lookup semantics remain unchanged;
|
||||
- full engine suite: 529 passed, 3 ignored manual benchmarks;
|
||||
- next-open execution-day risk, minute matching, fees, slippage, volume limits,
|
||||
corporate actions, delisting and futures tests all passed.
|
||||
|
||||
## Deployment Status
|
||||
|
||||
Not deployed. The 177 FIDC-managed Boris factor task is still active, so no
|
||||
Source Lake, backtest service or engine restart is allowed. After the task
|
||||
ends naturally, acceptance must use the same frozen bundle and compare daily
|
||||
selection, orders, fills, holdings, NAV, risk facts and canonical digest for
|
||||
multiple daily/minute and fixed/dynamic-universe strategies.
|
||||
Reference in New Issue
Block a user