diff --git a/crates/fidc-core/src/daily_patterns.rs b/crates/fidc-core/src/daily_patterns.rs index a49529f..e59bc8f 100644 --- a/crates/fidc-core/src/daily_patterns.rs +++ b/crates/fidc-core/src/daily_patterns.rs @@ -33,6 +33,31 @@ impl PatternSpec { if (self.template == "expression") != self.expression.is_some() { return Err("expression_template_requires_expression_only".into()); } + if let Some(expr) = &self.expression { + let supported = [ + "open", + "high", + "low", + "close", + "volume", + "raw_open", + "raw_high", + "raw_low", + "raw_close", + "prev_close", + "amount", + ]; + let missing = crate::factor_events::field_dependencies(expr) + .into_iter() + .filter(|f| !supported.contains(&f.as_str())) + .collect::>(); + if !missing.is_empty() { + return Err(format!( + "expression_source_mapping_required: {}", + missing.join(",") + )); + } + } let catalog = catalog(); let definition = catalog["templates"] .get(&self.template) @@ -92,6 +117,10 @@ pub struct PatternBar { pub low: Option, pub close: Option, pub volume: Option, + #[serde(default)] + pub prev_close: Option, + #[serde(default)] + pub amount: Option, pub adjustment_factor_backward1: Option, pub paused: Option, #[serde(default)] @@ -299,6 +328,37 @@ pub fn evaluate( .collect(), ); } + let needed = + crate::factor_events::field_dependencies(spec.expression.as_ref().unwrap()); + for name in ["prev_close", "amount"] { + if !needed.contains(name) { + continue; + } + let values = days + .iter() + .map(|d| { + let b = by_day[d]; + let value = number( + if name == "prev_close" { + b.prev_close + } else { + b.amount + }, + &series.symbol, + *d, + name, + )?; + if value < 0.0 || (name == "prev_close" && value == 0.0) { + return Err(format!( + "pattern_input_invalid: {} {d} {name}", + series.symbol + )); + } + Ok(Some(value)) + }) + .collect::, String>>()?; + fields.insert(name.into(), values); + } let frame = crate::factor_events::Frame { symbol: series.symbol.clone(), frequency: "1d".into(), @@ -321,7 +381,7 @@ pub fn evaluate( result.matched = latest == Some(1.0); result .checks - .push(json!({"label":"组合条件","passed":result.matched})); + .push(json!({"label":"组合条件","actual":latest,"operator":"==","threshold":1,"passed":result.matched})); } else { return Err("expression_signal_requires_boolean: 数值因子必须显式比较或组合,不能自动视为买卖信号".into()); } @@ -491,6 +551,8 @@ pub fn evaluate_dataset( low: Some(b.low), close: Some(b.close), volume: Some(b.volume as f64), + prev_close: data.factor_numeric_value(d, symbol, "pre_close"), + amount: data.factor_numeric_value(d, symbol, "amount"), adjustment_factor_backward1: data .factor(d, symbol) .and_then(|f| f.adjustment_factor_backward1), @@ -598,6 +660,8 @@ mod tests { low: Some(p), close: Some(p), volume: Some(100.0), + prev_close: Some(p - 1.0), + amount: Some(p * 100.0), adjustment_factor_backward1: Some(1.0), paused: Some(false), source_path: None, @@ -608,6 +672,19 @@ mod tests { let result = evaluate(&spec, &days, &series).unwrap(); assert!(result.matched); assert_eq!(result.score, Some(1.0)); + let vwap_spec = make( + json!({"kind":"operator","name":"GT","args":[{"kind":"operator","name":"DIV","args":[{"kind":"field","name":"amount"},{"kind":"field","name":"volume"}]},{"kind":"field","name":"prev_close"}]}), + ); + assert!(evaluate(&vwap_spec, &days, &series).unwrap().matched); + let mut missing_amount = series.clone(); + missing_amount.bars[1].amount = None; + assert!( + evaluate(&vwap_spec, &days, &missing_amount) + .unwrap_err() + .contains("amount") + ); + let mut missing_previous=series.clone();missing_previous.bars[1].prev_close=None; + assert!(evaluate(&vwap_spec,&days,&missing_previous).unwrap_err().contains("prev_close")); assert!( evaluate( &make(json!({"kind":"field","name":"close"})), @@ -646,6 +723,8 @@ mod tests { low: Some(c), close: Some(c), volume: Some(1000.0), + prev_close: Some(c - 1.0), + amount: Some(c * 1000.0), adjustment_factor_backward1: Some(1.0), paused: Some(false), source_path: Some("fixture.parquet".into()), diff --git a/crates/fidc-core/src/factor_events.rs b/crates/fidc-core/src/factor_events.rs index 7330693..a77237f 100644 --- a/crates/fidc-core/src/factor_events.rs +++ b/crates/fidc-core/src/factor_events.rs @@ -11,6 +11,27 @@ use ta_lib::{ pub const CONTRACT: &str = "fidc_factor_event_expression_v1"; pub const TA_REV: &str = "dd5a90259a3f9e04e2da9f38bf0719a841b40108"; +pub fn field_dependencies(expr: &Expr) -> std::collections::BTreeSet { + let mut fields = std::collections::BTreeSet::new(); + match expr { + Expr::Field { name } => { + fields.insert(name.clone()); + } + Expr::Indicator { inputs, .. } => { + for e in inputs { + fields.extend(field_dependencies(e)); + } + } + Expr::Operator { args, .. } => { + for e in args { + fields.extend(field_dependencies(e)); + } + } + _ => {} + } + fields +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum Expr {