复用选股日快照视图
This commit is contained in:
@@ -1335,6 +1335,55 @@ pub struct DataSet {
|
|||||||
futures_params_by_symbol: Arc<HashMap<String, Vec<FuturesTradingParameter>>>,
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub(crate) struct StandardRollingMeans {
|
pub(crate) struct StandardRollingMeans {
|
||||||
pub close: [Option<f64>; 7],
|
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> {
|
fn market_series(&self, symbol: &str) -> Option<&SymbolPriceSeries> {
|
||||||
self.market_series_by_symbol.get(symbol).map(Arc::as_ref)
|
self.market_series_by_symbol.get(symbol).map(Arc::as_ref)
|
||||||
}
|
}
|
||||||
@@ -4775,11 +4870,16 @@ mod tests {
|
|||||||
|
|
||||||
for symbol in ["000001.SZ", "600000.SH"] {
|
for symbol in ["000001.SZ", "600000.SH"] {
|
||||||
let symbol_id = data.symbol_id(symbol).unwrap();
|
let symbol_id = data.symbol_id(symbol).unwrap();
|
||||||
|
let day = data.daily_snapshot_view(date);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.market_by_symbol_id(date, symbol_id)
|
data.market_by_symbol_id(date, symbol_id)
|
||||||
.map(|row| row.symbol.as_str()),
|
.map(|row| row.symbol.as_str()),
|
||||||
Some(symbol)
|
Some(symbol)
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
day.market(symbol_id).map(|row| row.symbol.as_str()),
|
||||||
|
Some(symbol)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.factor_by_symbol_id(date, symbol_id)
|
data.factor_by_symbol_id(date, symbol_id)
|
||||||
.map(|row| row.symbol.as_str()),
|
.map(|row| row.symbol.as_str()),
|
||||||
@@ -4790,15 +4890,138 @@ mod tests {
|
|||||||
.map(|row| row.symbol.as_str()),
|
.map(|row| row.symbol.as_str()),
|
||||||
Some(symbol)
|
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 signal_id = data.symbol_id("000300.SH").unwrap();
|
||||||
|
let day = data.daily_snapshot_view(date);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.market_by_symbol_id(date, signal_id).map(|row| row.symbol.as_str()),
|
data.market_by_symbol_id(date, signal_id).map(|row| row.symbol.as_str()),
|
||||||
Some("000300.SH")
|
Some("000300.SH")
|
||||||
);
|
);
|
||||||
assert!(data.factor_by_symbol_id(date, signal_id).is_none());
|
assert!(data.factor_by_symbol_id(date, signal_id).is_none());
|
||||||
assert!(data.candidate_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]
|
#[test]
|
||||||
|
|||||||
@@ -8886,8 +8886,10 @@ impl PlatformExprStrategy {
|
|||||||
) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) {
|
) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
let mut decisions = Vec::new();
|
let mut decisions = Vec::new();
|
||||||
let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date);
|
let execution_day = ctx.data.daily_snapshot_view(date);
|
||||||
let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_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());
|
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
||||||
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
|
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) {
|
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
|
||||||
@@ -8895,14 +8897,14 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
let synthetic_candidate;
|
let synthetic_candidate;
|
||||||
let 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
|
candidate
|
||||||
} else {
|
} else {
|
||||||
synthetic_candidate =
|
synthetic_candidate =
|
||||||
crate::data::missing_candidate_risk_state(date, &factor.symbol);
|
crate::data::missing_candidate_risk_state(date, &factor.symbol);
|
||||||
&synthetic_candidate
|
&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;
|
continue;
|
||||||
};
|
};
|
||||||
let (reject_from_universe, selection_decision) = if collect_risk_decisions {
|
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