perf: reuse current rolling series boundary
This commit is contained in:
@@ -2012,6 +2012,14 @@ impl DataSet {
|
||||
Some(*end as usize)
|
||||
}
|
||||
|
||||
pub(crate) fn market_current_series_end_index_by_symbol_id(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
symbol_id: u32,
|
||||
) -> Option<usize> {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
}
|
||||
|
||||
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
|
||||
let symbol_id = self.symbol_id(symbol)?;
|
||||
self.factor_by_symbol_id(date, symbol_id)
|
||||
@@ -3347,20 +3355,56 @@ impl DataSet {
|
||||
symbol: &str,
|
||||
field: &str,
|
||||
lookback: usize,
|
||||
) -> Option<f64> {
|
||||
let normalized = normalized_field(field);
|
||||
let series_end = matches!(
|
||||
normalized.as_ref(),
|
||||
"close"
|
||||
| "prev_close"
|
||||
| "stock_close"
|
||||
| "price"
|
||||
| "volume"
|
||||
| "stock_volume"
|
||||
| "day_open"
|
||||
| "dayopen"
|
||||
| "open"
|
||||
| "last"
|
||||
| "last_price"
|
||||
)
|
||||
.then(|| self.market_current_series_end_index_by_symbol_id(date, symbol_id))
|
||||
.flatten();
|
||||
self.market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||
date,
|
||||
symbol_id,
|
||||
symbol,
|
||||
normalized.as_ref(),
|
||||
lookback,
|
||||
series_end,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
symbol_id: u32,
|
||||
symbol: &str,
|
||||
field: &str,
|
||||
lookback: usize,
|
||||
series_end: Option<usize>,
|
||||
) -> Option<f64> {
|
||||
let field = normalized_field(field);
|
||||
match field.as_ref() {
|
||||
"close" | "prev_close" | "stock_close" | "price" => self
|
||||
.adjusted_close_series_by_symbol_id(symbol_id)
|
||||
.and_then(|series| {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
series_end
|
||||
.map(|end| series.moving_average_at_end(end, lookback))
|
||||
.unwrap_or_else(|| series.current_moving_average(date, lookback))
|
||||
}),
|
||||
"volume" | "stock_volume" => {
|
||||
self.market_series_by_symbol_id(symbol_id)
|
||||
.and_then(|series| {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
series_end
|
||||
.map(|end| {
|
||||
series
|
||||
.valid_volume_window(end, lookback)
|
||||
@@ -3379,7 +3423,7 @@ impl DataSet {
|
||||
"day_open" | "dayopen" => {
|
||||
self.market_series_by_symbol_id(symbol_id)
|
||||
.and_then(|series| {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
series_end
|
||||
.map(|end| {
|
||||
series.moving_average_at_end(end, lookback, PriceField::DayOpen)
|
||||
})
|
||||
@@ -3391,14 +3435,14 @@ impl DataSet {
|
||||
"open" => self
|
||||
.market_series_by_symbol_id(symbol_id)
|
||||
.and_then(|series| {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
series_end
|
||||
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Open))
|
||||
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open))
|
||||
}),
|
||||
"last" | "last_price" => {
|
||||
self.market_series_by_symbol_id(symbol_id)
|
||||
.and_then(|series| {
|
||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
||||
series_end
|
||||
.map(|end| {
|
||||
series.moving_average_at_end(end, lookback, PriceField::Last)
|
||||
})
|
||||
@@ -5052,6 +5096,137 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual release-mode current rolling boundary benchmark"]
|
||||
fn benchmark_current_rolling_reuses_symbol_boundary() {
|
||||
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
|
||||
let dates = (0..160)
|
||||
.map(|offset| start + chrono::Duration::days(offset))
|
||||
.collect::<Vec<_>>();
|
||||
let data = DataSet::from_components(
|
||||
vec![Instrument {
|
||||
symbol: "000001.SZ".to_string(),
|
||||
name: "000001.SZ".to_string(),
|
||||
board: "SZ".to_string(),
|
||||
round_lot: 100,
|
||||
listed_at: None,
|
||||
delisted_at: None,
|
||||
status: "active".to_string(),
|
||||
}],
|
||||
dates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, date)| {
|
||||
market_row(
|
||||
&date.format("%Y-%m-%d").to_string(),
|
||||
10.0 + index as f64 / 100.0,
|
||||
100_000 + index as u64,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
dates
|
||||
.iter()
|
||||
.map(|date| DailyFactorSnapshot {
|
||||
date: *date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 8.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
extra_factors: NumericFactorMap::from([(
|
||||
Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD),
|
||||
1.0,
|
||||
)]),
|
||||
})
|
||||
.collect(),
|
||||
Vec::new(),
|
||||
dates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, date)| {
|
||||
benchmark_row(&date.format("%Y-%m-%d").to_string(), 1_000.0 + index as f64)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.unwrap();
|
||||
let date = *dates.last().unwrap();
|
||||
let symbol = "000001.SZ";
|
||||
let symbol_id = data.symbol_id(symbol).unwrap();
|
||||
let requirements = [
|
||||
("close", 5usize),
|
||||
("close", 10usize),
|
||||
("close", 30usize),
|
||||
("volume", 5usize),
|
||||
("volume", 100usize),
|
||||
];
|
||||
let iterations = 100_000usize;
|
||||
let mut repeated_nanos = 0u128;
|
||||
let mut reused_nanos = 0u128;
|
||||
let mut repeated_checksum = 0.0;
|
||||
let mut reused_checksum = 0.0;
|
||||
|
||||
for iteration in 0..iterations {
|
||||
if iteration % 2 == 0 {
|
||||
let started = std::time::Instant::now();
|
||||
for (field, lookback) in requirements {
|
||||
repeated_checksum += data
|
||||
.market_current_numeric_moving_average_by_symbol_id(
|
||||
date, symbol_id, symbol, field, lookback,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
repeated_nanos += started.elapsed().as_nanos();
|
||||
let started = std::time::Instant::now();
|
||||
let series_end = data.market_current_series_end_index_by_symbol_id(date, symbol_id);
|
||||
for (field, lookback) in requirements {
|
||||
reused_checksum += data
|
||||
.market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||
date, symbol_id, symbol, field, lookback, series_end,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
reused_nanos += started.elapsed().as_nanos();
|
||||
} else {
|
||||
let started = std::time::Instant::now();
|
||||
let series_end = data.market_current_series_end_index_by_symbol_id(date, symbol_id);
|
||||
for (field, lookback) in requirements {
|
||||
reused_checksum += data
|
||||
.market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||
date, symbol_id, symbol, field, lookback, series_end,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
reused_nanos += started.elapsed().as_nanos();
|
||||
let started = std::time::Instant::now();
|
||||
for (field, lookback) in requirements {
|
||||
repeated_checksum += data
|
||||
.market_current_numeric_moving_average_by_symbol_id(
|
||||
date, symbol_id, symbol, field, lookback,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
repeated_nanos += started.elapsed().as_nanos();
|
||||
}
|
||||
}
|
||||
|
||||
assert!((repeated_checksum - reused_checksum).abs() < 1e-6);
|
||||
let repeated_seconds = repeated_nanos as f64 / 1_000_000_000.0;
|
||||
let reused_seconds = reused_nanos as f64 / 1_000_000_000.0;
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"schemaVersion": "fidc-current-rolling-boundary-benchmark/v1",
|
||||
"iterations": iterations,
|
||||
"helperCallsPerIteration": requirements.len(),
|
||||
"repeatedLookupSeconds": repeated_seconds,
|
||||
"reusedBoundarySeconds": reused_seconds,
|
||||
"speedup": repeated_seconds / reused_seconds,
|
||||
"equal": true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual component benchmark"]
|
||||
fn benchmark_daily_snapshot_view_lookup() {
|
||||
|
||||
@@ -718,6 +718,7 @@ struct StockExpressionState {
|
||||
stock_volume_ma20: f64,
|
||||
stock_volume_ma60: f64,
|
||||
stock_volume_ma100: f64,
|
||||
current_series_end: Option<usize>,
|
||||
extra_factors: BTreeMap<String, f64>,
|
||||
extra_text_factors: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -1044,6 +1045,7 @@ pub struct PlatformExprStrategy {
|
||||
stock_filter_expr_present: bool,
|
||||
selection_quote_usage: StockFilterQuoteUsage,
|
||||
stock_rolling_requirements: StockRollingRequirements,
|
||||
stock_current_rolling_mean_required: bool,
|
||||
stock_extra_factors_required: bool,
|
||||
stock_extra_factor_identifiers: BTreeSet<String>,
|
||||
stock_extra_factor_map_required: bool,
|
||||
@@ -1321,6 +1323,8 @@ impl PlatformExprStrategy {
|
||||
let selection_quote_usage =
|
||||
Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr);
|
||||
let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config);
|
||||
let stock_current_rolling_mean_required =
|
||||
Self::stock_current_rolling_mean_required_for_config(&config);
|
||||
let stock_extra_factors_required =
|
||||
Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers);
|
||||
let stock_extra_factor_identifiers =
|
||||
@@ -1363,6 +1367,7 @@ impl PlatformExprStrategy {
|
||||
stock_filter_expr_present,
|
||||
selection_quote_usage,
|
||||
stock_rolling_requirements,
|
||||
stock_current_rolling_mean_required,
|
||||
stock_extra_factors_required,
|
||||
stock_extra_factor_identifiers,
|
||||
stock_extra_factor_map_required,
|
||||
@@ -3913,14 +3918,19 @@ impl PlatformExprStrategy {
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
symbol_id: u32,
|
||||
symbol: &str,
|
||||
stock: &StockExpressionState,
|
||||
field: &str,
|
||||
lookback: usize,
|
||||
) -> Option<f64> {
|
||||
ctx.data.market_current_numeric_moving_average_by_symbol_id(
|
||||
date, symbol_id, symbol, field, lookback,
|
||||
)
|
||||
ctx.data
|
||||
.market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||
date,
|
||||
stock.symbol_id,
|
||||
&stock.symbol,
|
||||
field,
|
||||
lookback,
|
||||
stock.current_series_end,
|
||||
)
|
||||
}
|
||||
|
||||
fn stock_state_at_time(
|
||||
@@ -4065,6 +4075,13 @@ impl PlatformExprStrategy {
|
||||
None
|
||||
};
|
||||
let instrument = ctx.data.instrument_by_symbol_id(symbol_id);
|
||||
let current_series_end = self
|
||||
.stock_current_rolling_mean_required
|
||||
.then(|| {
|
||||
ctx.data
|
||||
.market_current_series_end_index_by_symbol_id(date, symbol_id)
|
||||
})
|
||||
.flatten();
|
||||
let required_rolling = |field: &'static str, lookback: usize| {
|
||||
self.stock_rolling_requirements
|
||||
.requires(field, lookback)
|
||||
@@ -4258,6 +4275,7 @@ impl PlatformExprStrategy {
|
||||
stock_volume_ma20,
|
||||
stock_volume_ma60,
|
||||
stock_volume_ma100,
|
||||
current_series_end,
|
||||
extra_factors,
|
||||
extra_text_factors: if self.stock_text_factors_required {
|
||||
ctx.data
|
||||
@@ -7091,14 +7109,7 @@ impl PlatformExprStrategy {
|
||||
"rolling_mean_current(\"{other}\", {lookback}) requires stock context"
|
||||
))
|
||||
})?;
|
||||
self.stock_current_rolling_mean(
|
||||
ctx,
|
||||
day.date,
|
||||
stock.symbol_id,
|
||||
&stock.symbol,
|
||||
other,
|
||||
lookback,
|
||||
)
|
||||
self.stock_current_rolling_mean(ctx, day.date, stock, other, lookback)
|
||||
}
|
||||
};
|
||||
value.ok_or_else(|| {
|
||||
@@ -9993,6 +10004,30 @@ impl PlatformExprStrategy {
|
||||
requirements
|
||||
}
|
||||
|
||||
fn stock_current_rolling_mean_required_for_config(config: &PlatformExprStrategyConfig) -> bool {
|
||||
let mut requirements = StockRollingRequirements::default();
|
||||
for expr in [
|
||||
config.prelude.as_str(),
|
||||
config.stock_filter_expr.as_str(),
|
||||
config.buy_scale_expr.as_str(),
|
||||
config.stop_loss_expr.as_str(),
|
||||
config.take_profit_expr.as_str(),
|
||||
config.rank_expr.as_str(),
|
||||
config.market_cap_field.as_str(),
|
||||
] {
|
||||
let compact = Self::compact_expr(&Self::normalize_expr(expr));
|
||||
Self::require_stock_rollings_for_named_helper(
|
||||
&mut requirements,
|
||||
&compact,
|
||||
"rolling_mean_current",
|
||||
);
|
||||
if !requirements.fields.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn stock_extra_factors_required_for_config(
|
||||
config: &PlatformExprStrategyConfig,
|
||||
prelude_declared_identifiers: &BTreeSet<String>,
|
||||
@@ -14272,6 +14307,16 @@ mod tests {
|
||||
assert!(!strategy.stock_rolling_requirements.requires("close", 10));
|
||||
assert!(!strategy.stock_rolling_requirements.requires("volume", 5));
|
||||
assert!(!strategy.stock_rolling_requirements.requires("volume", 100));
|
||||
assert!(strategy.stock_current_rolling_mean_required);
|
||||
|
||||
let mut signal_only = PlatformExprStrategyConfig::microcap_rotation();
|
||||
signal_only.stock_filter_expr = "true".to_string();
|
||||
signal_only.prelude =
|
||||
"let signal_ma = rolling_mean_current(\"signal_close\", 20);".to_string();
|
||||
assert!(
|
||||
!PlatformExprStrategy::new(signal_only).stock_current_rolling_mean_required,
|
||||
"signal-only rolling helpers must not add per-stock current boundaries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user