perf: reuse current rolling series boundary
This commit is contained in:
@@ -2012,6 +2012,14 @@ impl DataSet {
|
|||||||
Some(*end as usize)
|
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> {
|
pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> {
|
||||||
let symbol_id = self.symbol_id(symbol)?;
|
let symbol_id = self.symbol_id(symbol)?;
|
||||||
self.factor_by_symbol_id(date, symbol_id)
|
self.factor_by_symbol_id(date, symbol_id)
|
||||||
@@ -3347,20 +3355,56 @@ impl DataSet {
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
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> {
|
) -> Option<f64> {
|
||||||
let field = normalized_field(field);
|
let field = normalized_field(field);
|
||||||
match field.as_ref() {
|
match field.as_ref() {
|
||||||
"close" | "prev_close" | "stock_close" | "price" => self
|
"close" | "prev_close" | "stock_close" | "price" => self
|
||||||
.adjusted_close_series_by_symbol_id(symbol_id)
|
.adjusted_close_series_by_symbol_id(symbol_id)
|
||||||
.and_then(|series| {
|
.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))
|
.map(|end| series.moving_average_at_end(end, lookback))
|
||||||
.unwrap_or_else(|| series.current_moving_average(date, lookback))
|
.unwrap_or_else(|| series.current_moving_average(date, lookback))
|
||||||
}),
|
}),
|
||||||
"volume" | "stock_volume" => {
|
"volume" | "stock_volume" => {
|
||||||
self.market_series_by_symbol_id(symbol_id)
|
self.market_series_by_symbol_id(symbol_id)
|
||||||
.and_then(|series| {
|
.and_then(|series| {
|
||||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
series_end
|
||||||
.map(|end| {
|
.map(|end| {
|
||||||
series
|
series
|
||||||
.valid_volume_window(end, lookback)
|
.valid_volume_window(end, lookback)
|
||||||
@@ -3379,7 +3423,7 @@ impl DataSet {
|
|||||||
"day_open" | "dayopen" => {
|
"day_open" | "dayopen" => {
|
||||||
self.market_series_by_symbol_id(symbol_id)
|
self.market_series_by_symbol_id(symbol_id)
|
||||||
.and_then(|series| {
|
.and_then(|series| {
|
||||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
series_end
|
||||||
.map(|end| {
|
.map(|end| {
|
||||||
series.moving_average_at_end(end, lookback, PriceField::DayOpen)
|
series.moving_average_at_end(end, lookback, PriceField::DayOpen)
|
||||||
})
|
})
|
||||||
@@ -3391,14 +3435,14 @@ impl DataSet {
|
|||||||
"open" => self
|
"open" => self
|
||||||
.market_series_by_symbol_id(symbol_id)
|
.market_series_by_symbol_id(symbol_id)
|
||||||
.and_then(|series| {
|
.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))
|
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Open))
|
||||||
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open))
|
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open))
|
||||||
}),
|
}),
|
||||||
"last" | "last_price" => {
|
"last" | "last_price" => {
|
||||||
self.market_series_by_symbol_id(symbol_id)
|
self.market_series_by_symbol_id(symbol_id)
|
||||||
.and_then(|series| {
|
.and_then(|series| {
|
||||||
self.market_series_end_index_by_symbol_id(date, symbol_id, true)
|
series_end
|
||||||
.map(|end| {
|
.map(|end| {
|
||||||
series.moving_average_at_end(end, lookback, PriceField::Last)
|
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]
|
#[test]
|
||||||
#[ignore = "manual component benchmark"]
|
#[ignore = "manual component benchmark"]
|
||||||
fn benchmark_daily_snapshot_view_lookup() {
|
fn benchmark_daily_snapshot_view_lookup() {
|
||||||
|
|||||||
@@ -718,6 +718,7 @@ struct StockExpressionState {
|
|||||||
stock_volume_ma20: f64,
|
stock_volume_ma20: f64,
|
||||||
stock_volume_ma60: f64,
|
stock_volume_ma60: f64,
|
||||||
stock_volume_ma100: f64,
|
stock_volume_ma100: f64,
|
||||||
|
current_series_end: Option<usize>,
|
||||||
extra_factors: BTreeMap<String, f64>,
|
extra_factors: BTreeMap<String, f64>,
|
||||||
extra_text_factors: BTreeMap<String, String>,
|
extra_text_factors: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
@@ -1044,6 +1045,7 @@ pub struct PlatformExprStrategy {
|
|||||||
stock_filter_expr_present: bool,
|
stock_filter_expr_present: bool,
|
||||||
selection_quote_usage: StockFilterQuoteUsage,
|
selection_quote_usage: StockFilterQuoteUsage,
|
||||||
stock_rolling_requirements: StockRollingRequirements,
|
stock_rolling_requirements: StockRollingRequirements,
|
||||||
|
stock_current_rolling_mean_required: bool,
|
||||||
stock_extra_factors_required: bool,
|
stock_extra_factors_required: bool,
|
||||||
stock_extra_factor_identifiers: BTreeSet<String>,
|
stock_extra_factor_identifiers: BTreeSet<String>,
|
||||||
stock_extra_factor_map_required: bool,
|
stock_extra_factor_map_required: bool,
|
||||||
@@ -1321,6 +1323,8 @@ impl PlatformExprStrategy {
|
|||||||
let selection_quote_usage =
|
let selection_quote_usage =
|
||||||
Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr);
|
Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr);
|
||||||
let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config);
|
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 =
|
let stock_extra_factors_required =
|
||||||
Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers);
|
Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers);
|
||||||
let stock_extra_factor_identifiers =
|
let stock_extra_factor_identifiers =
|
||||||
@@ -1363,6 +1367,7 @@ impl PlatformExprStrategy {
|
|||||||
stock_filter_expr_present,
|
stock_filter_expr_present,
|
||||||
selection_quote_usage,
|
selection_quote_usage,
|
||||||
stock_rolling_requirements,
|
stock_rolling_requirements,
|
||||||
|
stock_current_rolling_mean_required,
|
||||||
stock_extra_factors_required,
|
stock_extra_factors_required,
|
||||||
stock_extra_factor_identifiers,
|
stock_extra_factor_identifiers,
|
||||||
stock_extra_factor_map_required,
|
stock_extra_factor_map_required,
|
||||||
@@ -3913,13 +3918,18 @@ impl PlatformExprStrategy {
|
|||||||
&self,
|
&self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
symbol_id: u32,
|
stock: &StockExpressionState,
|
||||||
symbol: &str,
|
|
||||||
field: &str,
|
field: &str,
|
||||||
lookback: usize,
|
lookback: usize,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
ctx.data.market_current_numeric_moving_average_by_symbol_id(
|
ctx.data
|
||||||
date, symbol_id, symbol, field, lookback,
|
.market_current_numeric_moving_average_with_end_by_symbol_id(
|
||||||
|
date,
|
||||||
|
stock.symbol_id,
|
||||||
|
&stock.symbol,
|
||||||
|
field,
|
||||||
|
lookback,
|
||||||
|
stock.current_series_end,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4065,6 +4075,13 @@ impl PlatformExprStrategy {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let instrument = ctx.data.instrument_by_symbol_id(symbol_id);
|
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| {
|
let required_rolling = |field: &'static str, lookback: usize| {
|
||||||
self.stock_rolling_requirements
|
self.stock_rolling_requirements
|
||||||
.requires(field, lookback)
|
.requires(field, lookback)
|
||||||
@@ -4258,6 +4275,7 @@ impl PlatformExprStrategy {
|
|||||||
stock_volume_ma20,
|
stock_volume_ma20,
|
||||||
stock_volume_ma60,
|
stock_volume_ma60,
|
||||||
stock_volume_ma100,
|
stock_volume_ma100,
|
||||||
|
current_series_end,
|
||||||
extra_factors,
|
extra_factors,
|
||||||
extra_text_factors: if self.stock_text_factors_required {
|
extra_text_factors: if self.stock_text_factors_required {
|
||||||
ctx.data
|
ctx.data
|
||||||
@@ -7091,14 +7109,7 @@ impl PlatformExprStrategy {
|
|||||||
"rolling_mean_current(\"{other}\", {lookback}) requires stock context"
|
"rolling_mean_current(\"{other}\", {lookback}) requires stock context"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
self.stock_current_rolling_mean(
|
self.stock_current_rolling_mean(ctx, day.date, stock, other, lookback)
|
||||||
ctx,
|
|
||||||
day.date,
|
|
||||||
stock.symbol_id,
|
|
||||||
&stock.symbol,
|
|
||||||
other,
|
|
||||||
lookback,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
value.ok_or_else(|| {
|
value.ok_or_else(|| {
|
||||||
@@ -9993,6 +10004,30 @@ impl PlatformExprStrategy {
|
|||||||
requirements
|
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(
|
fn stock_extra_factors_required_for_config(
|
||||||
config: &PlatformExprStrategyConfig,
|
config: &PlatformExprStrategyConfig,
|
||||||
prelude_declared_identifiers: &BTreeSet<String>,
|
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("close", 10));
|
||||||
assert!(!strategy.stock_rolling_requirements.requires("volume", 5));
|
assert!(!strategy.stock_rolling_requirements.requires("volume", 5));
|
||||||
assert!(!strategy.stock_rolling_requirements.requires("volume", 100));
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user