perf: type static current rolling helpers

This commit is contained in:
boris
2026-09-05 02:42:47 +08:00
parent 1410aa588a
commit 75ab0c06c6
2 changed files with 126 additions and 26 deletions
+48 -25
View File
@@ -3454,31 +3454,13 @@ impl DataSet {
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) .market_current_close_moving_average_with_end_by_symbol_id(
.and_then(|series| { date, symbol_id, lookback, series_end,
series_end ),
.map(|end| series.moving_average_at_end(end, lookback)) "volume" | "stock_volume" => self
.unwrap_or_else(|| series.current_moving_average(date, lookback)) .market_current_volume_moving_average_with_end_by_symbol_id(
}), date, symbol_id, lookback, series_end,
"volume" | "stock_volume" => { ),
self.market_series_by_symbol_id(symbol_id)
.and_then(|series| {
series_end
.map(|end| {
series
.valid_volume_window(end, lookback)
.map(|(start, end)| {
normalize_rolling_factor(
(series.valid_volume_sum_prefix[end]
- series.valid_volume_sum_prefix[start])
/ lookback as f64,
12,
)
})
})
.unwrap_or_else(|| series.current_volume_moving_average(date, lookback))
})
}
"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| {
@@ -3514,6 +3496,47 @@ impl DataSet {
} }
} }
pub(crate) fn market_current_close_moving_average_with_end_by_symbol_id(
&self,
date: NaiveDate,
symbol_id: u32,
lookback: usize,
series_end: Option<usize>,
) -> Option<f64> {
self.adjusted_close_series_by_symbol_id(symbol_id)
.and_then(|series| {
series_end
.map(|end| series.moving_average_at_end(end, lookback))
.unwrap_or_else(|| series.current_moving_average(date, lookback))
})
}
pub(crate) fn market_current_volume_moving_average_with_end_by_symbol_id(
&self,
date: NaiveDate,
symbol_id: u32,
lookback: usize,
series_end: Option<usize>,
) -> Option<f64> {
self.market_series_by_symbol_id(symbol_id)
.and_then(|series| {
series_end
.map(|end| {
series
.valid_volume_window(end, lookback)
.map(|(start, end)| {
normalize_rolling_factor(
(series.valid_volume_sum_prefix[end]
- series.valid_volume_sum_prefix[start])
/ lookback as f64,
12,
)
})
})
.unwrap_or_else(|| series.current_volume_moving_average(date, lookback))
})
}
pub fn market_latest_back_adjusted_close(&self, date: NaiveDate, symbol: &str) -> Option<f64> { pub fn market_latest_back_adjusted_close(&self, date: NaiveDate, symbol: &str) -> Option<f64> {
self.adjusted_close_series(symbol) self.adjusted_close_series(symbol)
.and_then(|series| series.latest_back_adjusted_close(date)) .and_then(|series| series.latest_back_adjusted_close(date))
+78 -1
View File
@@ -972,6 +972,10 @@ enum CompiledRuntimeHelperArgs {
lookback: usize, lookback: usize,
current: bool, current: bool,
}, },
StockCurrentRollingMean {
field: StockRollingField,
lookback: usize,
},
RollingMaxCurrent { RollingMaxCurrent {
field: String, field: String,
lookback: usize, lookback: usize,
@@ -5124,6 +5128,41 @@ impl PlatformExprStrategy {
}; };
Ok(RuntimeHelperResolution::Number(value)) Ok(RuntimeHelperResolution::Number(value))
} }
CompiledRuntimeHelperArgs::StockCurrentRollingMean { field, lookback } => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!(
"rolling_mean_current requires stock context for lookback {lookback}"
))
})?;
let value = match field {
StockRollingField::Close => ctx
.data
.market_current_close_moving_average_with_end_by_symbol_id(
day.date,
stock.symbol_id,
*lookback,
stock.current_series_end,
),
StockRollingField::Volume => ctx
.data
.market_current_volume_moving_average_with_end_by_symbol_id(
day.date,
stock.symbol_id,
*lookback,
stock.current_series_end,
),
}
.ok_or_else(|| {
BacktestError::Execution(format!(
"missing current rolling mean for field {} with lookback {lookback}",
match field {
StockRollingField::Close => "close",
StockRollingField::Volume => "volume",
}
))
})?;
Ok(RuntimeHelperResolution::Number(value))
}
CompiledRuntimeHelperArgs::RollingMaxCurrent { field, lookback } => { CompiledRuntimeHelperArgs::RollingMaxCurrent { field, lookback } => {
let values = let values =
self.resolve_current_rolling_values(ctx, day, stock, field, *lookback)?; self.resolve_current_rolling_values(ctx, day, stock, field, *lookback)?;
@@ -6216,6 +6255,19 @@ impl PlatformExprStrategy {
} }
"rolling_mean_current" => { "rolling_mean_current" => {
let (field, lookback) = field_lookback()?; let (field, lookback) = field_lookback()?;
let stock_field = match field.trim().to_ascii_lowercase().as_str() {
"close" | "prev_close" | "stock_close" | "price" => {
Some(StockRollingField::Close)
}
"volume" | "stock_volume" => Some(StockRollingField::Volume),
_ => None,
};
if let Some(field) = stock_field {
return Some(CompiledRuntimeHelperArgs::StockCurrentRollingMean {
field,
lookback,
});
}
Some(CompiledRuntimeHelperArgs::RollingMean { Some(CompiledRuntimeHelperArgs::RollingMean {
field, field,
lookback, lookback,
@@ -12675,7 +12727,7 @@ mod tests {
PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig, PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig,
PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency,
PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind,
RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, StockRollingField,
StockSnapshotFieldRequirements, framework_stock_rolling_factor_requirement, StockSnapshotFieldRequirements, framework_stock_rolling_factor_requirement,
scheduled_position_exposure, scheduled_position_exposure,
}; };
@@ -33593,6 +33645,31 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0;
.all(|binding| binding.compiled_args.is_some()), .all(|binding| binding.compiled_args.is_some()),
"all static numeric helper arguments must be parsed once at plan compilation" "all static numeric helper arguments must be parsed once at plan compilation"
); );
assert!(bindings.iter().any(|binding| {
if binding.name != "rolling_mean_current" {
return false;
}
matches!(
binding.compiled_args.as_ref(),
Some(CompiledRuntimeHelperArgs::StockCurrentRollingMean {
field: StockRollingField::Volume,
lookback: 10,
})
)
}));
let signal_args = vec!["\"signal_close\"".to_string(), "20".to_string()];
assert!(matches!(
PlatformExprStrategy::compile_runtime_helper_args(
"rolling_mean_current",
&signal_args
),
Some(CompiledRuntimeHelperArgs::RollingMean {
field,
lookback: 20,
current: true,
}) if field == "signal_close"
));
} }
#[test] #[test]