Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30e8227099 |
@@ -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::<Vec<_>>();
|
||||
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<f64>,
|
||||
pub close: 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 paused: Option<bool>,
|
||||
#[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::<Result<Vec<_>, 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()),
|
||||
|
||||
@@ -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<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)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Expr {
|
||||
|
||||
Reference in New Issue
Block a user