Compare commits

..

1 Commits

Author SHA1 Message Date
boris 30e8227099 接通真实昨收成交额并前置校验表达式字段 2026-09-09 11:33:58 +08:00
2 changed files with 101 additions and 1 deletions
+80 -1
View File
@@ -33,6 +33,31 @@ impl PatternSpec {
if (self.template == "expression") != self.expression.is_some() { if (self.template == "expression") != self.expression.is_some() {
return Err("expression_template_requires_expression_only".into()); 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::<Vec<_>>();
if !missing.is_empty() {
return Err(format!(
"expression_source_mapping_required: {}",
missing.join(",")
));
}
}
let catalog = catalog(); let catalog = catalog();
let definition = catalog["templates"] let definition = catalog["templates"]
.get(&self.template) .get(&self.template)
@@ -92,6 +117,10 @@ pub struct PatternBar {
pub low: Option<f64>, pub low: Option<f64>,
pub close: Option<f64>, pub close: Option<f64>,
pub volume: Option<f64>, pub volume: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
pub adjustment_factor_backward1: Option<f64>, pub adjustment_factor_backward1: Option<f64>,
pub paused: Option<bool>, pub paused: Option<bool>,
#[serde(default)] #[serde(default)]
@@ -299,6 +328,37 @@ pub fn evaluate(
.collect(), .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::<Result<Vec<_>, String>>()?;
fields.insert(name.into(), values);
}
let frame = crate::factor_events::Frame { let frame = crate::factor_events::Frame {
symbol: series.symbol.clone(), symbol: series.symbol.clone(),
frequency: "1d".into(), frequency: "1d".into(),
@@ -321,7 +381,7 @@ pub fn evaluate(
result.matched = latest == Some(1.0); result.matched = latest == Some(1.0);
result result
.checks .checks
.push(json!({"label":"组合条件","passed":result.matched})); .push(json!({"label":"组合条件","actual":latest,"operator":"==","threshold":1,"passed":result.matched}));
} else { } else {
return Err("expression_signal_requires_boolean: 数值因子必须显式比较或组合,不能自动视为买卖信号".into()); return Err("expression_signal_requires_boolean: 数值因子必须显式比较或组合,不能自动视为买卖信号".into());
} }
@@ -491,6 +551,8 @@ pub fn evaluate_dataset(
low: Some(b.low), low: Some(b.low),
close: Some(b.close), close: Some(b.close),
volume: Some(b.volume as f64), 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 adjustment_factor_backward1: data
.factor(d, symbol) .factor(d, symbol)
.and_then(|f| f.adjustment_factor_backward1), .and_then(|f| f.adjustment_factor_backward1),
@@ -598,6 +660,8 @@ mod tests {
low: Some(p), low: Some(p),
close: Some(p), close: Some(p),
volume: Some(100.0), volume: Some(100.0),
prev_close: Some(p - 1.0),
amount: Some(p * 100.0),
adjustment_factor_backward1: Some(1.0), adjustment_factor_backward1: Some(1.0),
paused: Some(false), paused: Some(false),
source_path: None, source_path: None,
@@ -608,6 +672,19 @@ mod tests {
let result = evaluate(&spec, &days, &series).unwrap(); let result = evaluate(&spec, &days, &series).unwrap();
assert!(result.matched); assert!(result.matched);
assert_eq!(result.score, Some(1.0)); 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!( assert!(
evaluate( evaluate(
&make(json!({"kind":"field","name":"close"})), &make(json!({"kind":"field","name":"close"})),
@@ -646,6 +723,8 @@ mod tests {
low: Some(c), low: Some(c),
close: Some(c), close: Some(c),
volume: Some(1000.0), volume: Some(1000.0),
prev_close: Some(c - 1.0),
amount: Some(c * 1000.0),
adjustment_factor_backward1: Some(1.0), adjustment_factor_backward1: Some(1.0),
paused: Some(false), paused: Some(false),
source_path: Some("fixture.parquet".into()), source_path: Some("fixture.parquet".into()),
+21
View File
@@ -11,6 +11,27 @@ use ta_lib::{
pub const CONTRACT: &str = "fidc_factor_event_expression_v1"; pub const CONTRACT: &str = "fidc_factor_event_expression_v1";
pub const TA_REV: &str = "dd5a90259a3f9e04e2da9f38bf0719a841b40108"; pub const TA_REV: &str = "dd5a90259a3f9e04e2da9f38bf0719a841b40108";
pub fn field_dependencies(expr: &Expr) -> std::collections::BTreeSet<String> {
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)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Expr { pub enum Expr {