按表达式裁剪股票rolling状态

This commit is contained in:
boris
2026-06-21 01:01:23 +08:00
parent fd27429713
commit 78af8c3219
+154 -1
View File
@@ -442,6 +442,42 @@ enum StockFilterQuoteUsage {
IntradayQuote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum StockRollingField {
Close,
Volume,
}
#[derive(Debug, Clone, Default)]
struct StockRollingRequirements {
all: bool,
fields: BTreeSet<(StockRollingField, usize)>,
}
impl StockRollingRequirements {
fn require(&mut self, field: StockRollingField, lookback: usize) {
if lookback > 0 {
self.fields.insert((field, lookback));
}
}
fn require_all(&mut self) {
self.all = true;
}
fn requires(&self, field: &'static str, lookback: usize) -> bool {
if self.all {
return true;
}
let field = match field {
"close" => StockRollingField::Close,
"volume" => StockRollingField::Volume,
_ => return false,
};
self.fields.contains(&(field, lookback))
}
}
#[derive(Debug, Clone)]
struct PositionExpressionState {
order_book_id: String,
@@ -517,6 +553,7 @@ pub struct PlatformExprStrategy {
prelude_numeric_constants: HashMap<String, f64>,
compact_stock_filter_expr: String,
stock_filter_quote_usage: StockFilterQuoteUsage,
stock_rolling_requirements: StockRollingRequirements,
stock_state_cache_date: RefCell<Option<NaiveDate>>,
stock_state_cache:
RefCell<HashMap<(NaiveDate, NaiveDate, String, Option<NaiveTime>), StockExpressionState>>,
@@ -607,6 +644,8 @@ impl PlatformExprStrategy {
let compact_stock_filter_expr = Self::compact_expr(&normalized_stock_filter_expr);
let stock_filter_quote_usage =
Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr);
let stock_rolling_requirements =
Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr);
Self {
config,
engine,
@@ -623,6 +662,7 @@ impl PlatformExprStrategy {
prelude_numeric_constants,
compact_stock_filter_expr,
stock_filter_quote_usage,
stock_rolling_requirements,
stock_state_cache_date: RefCell::new(None),
stock_state_cache: RefCell::new(HashMap::new()),
}
@@ -2266,6 +2306,9 @@ impl PlatformExprStrategy {
let instrument = ctx.data.instrument(symbol);
let mut rolling_values = HashMap::<(&'static str, usize), f64>::new();
let mut rolling = |field: &'static str, lookback: usize| -> f64 {
if !self.stock_rolling_requirements.requires(field, lookback) {
return f64::NAN;
}
*rolling_values.entry((field, lookback)).or_insert_with(|| {
self.stock_decision_rolling_mean(
ctx,
@@ -6189,6 +6232,113 @@ impl PlatformExprStrategy {
usage
}
fn stock_rolling_requirements_for_config(
config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
) -> StockRollingRequirements {
let mut requirements = StockRollingRequirements::default();
let expressions = [
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(),
];
for expr in expressions {
let normalized = Self::normalize_expr(expr);
if Self::extract_identifier_candidates(&normalized).contains("factors") {
requirements.require_all();
return requirements;
}
Self::require_stock_rollings_for_identifiers(&mut requirements, config, &normalized);
}
Self::require_stock_rollings_for_fast_filter(
&mut requirements,
config,
normalized_stock_filter_expr,
);
requirements
}
fn require_stock_rollings_for_identifiers(
requirements: &mut StockRollingRequirements,
config: &PlatformExprStrategyConfig,
expr: &str,
) {
for name in Self::extract_identifier_candidates(expr) {
match name.as_str() {
"stock_ma_short" => {
requirements.require(StockRollingField::Close, config.stock_short_ma_days)
}
"stock_ma_mid" => {
requirements.require(StockRollingField::Close, config.stock_mid_ma_days)
}
"stock_ma_long" => {
requirements.require(StockRollingField::Close, config.stock_long_ma_days)
}
"stock_ma5" | "ma5" => requirements.require(StockRollingField::Close, 5),
"stock_ma10" | "ma10" => requirements.require(StockRollingField::Close, 10),
"stock_ma20" | "ma20" => requirements.require(StockRollingField::Close, 20),
"stock_ma30" | "ma30" => requirements.require(StockRollingField::Close, 30),
"stock_volume_ma5" | "volume_ma5" | "vma5" => {
requirements.require(StockRollingField::Volume, 5)
}
"stock_volume_ma10" | "volume_ma10" | "vma10" => {
requirements.require(StockRollingField::Volume, 10)
}
"stock_volume_ma20" | "volume_ma20" | "vma20" => {
requirements.require(StockRollingField::Volume, 20)
}
"stock_volume_ma60" | "volume_ma60" | "vma60" => {
requirements.require(StockRollingField::Volume, 60)
}
"stock_volume_ma100" | "volume_ma100" | "vma100" => {
requirements.require(StockRollingField::Volume, 100)
}
_ => {}
}
}
}
fn require_stock_rollings_for_fast_filter(
requirements: &mut StockRollingRequirements,
config: &PlatformExprStrategyConfig,
normalized_stock_filter_expr: &str,
) {
let compact = Self::compact_expr(normalized_stock_filter_expr);
if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" {
requirements.require(StockRollingField::Close, config.stock_short_ma_days);
requirements.require(StockRollingField::Close, config.stock_mid_ma_days);
requirements.require(StockRollingField::Close, config.stock_long_ma_days);
return;
}
let mut filter_body = compact.as_str();
if let Some(rest) = filter_body.strip_prefix("listed_days>=min_listed_days&&") {
filter_body = rest;
}
let base_microcap_filter = "rolling_mean(\"close\",5)>rolling_mean(\"close\",10)*ma_ratio&&rolling_mean(\"close\",10)>rolling_mean(\"close\",30)*ma_ratio&&rolling_mean(\"volume\",5)<rolling_mean(\"volume\",100)*max_volume_ratio";
let matches_microcap_fast_filter = filter_body == base_microcap_filter
|| filter_body
.strip_prefix(base_microcap_filter)
.is_some_and(|tail| {
matches!(
tail,
"&&rolling_mean(\"volume\",5)>0&&rolling_mean(\"volume\",100)>0"
| "&&rolling_mean(\"volume\",5)>0.0&&rolling_mean(\"volume\",100)>0.0"
)
});
if matches_microcap_fast_filter {
requirements.require(StockRollingField::Close, 5);
requirements.require(StockRollingField::Close, 10);
requirements.require(StockRollingField::Close, 30);
requirements.require(StockRollingField::Volume, 5);
requirements.require(StockRollingField::Volume, 100);
}
}
fn stock_filter_uses_intraday_quote_fields(&self) -> bool {
self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly
}
@@ -18735,6 +18885,7 @@ mod tests {
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.prefer_precomputed_rolling_factors = true;
cfg.stock_filter_expr = "stock_ma5 > 0 && stock_volume_ma5 > 0".to_string();
let strategy = PlatformExprStrategy::new(cfg);
let stock = strategy
.stock_state_with_factor_date(&ctx, date, date, symbol)
@@ -18742,7 +18893,9 @@ mod tests {
assert_eq!(stock.stock_ma5, 99.0);
assert_eq!(stock.stock_volume_ma5, 88.0);
let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.stock_filter_expr = "stock_ma5 > 0 && stock_volume_ma5 > 0".to_string();
let strategy = PlatformExprStrategy::new(cfg);
let stock = strategy
.stock_state_with_factor_date(&ctx, date, date, symbol)
.expect("stock state");