接通真实昨收成交额并前置校验表达式字段
This commit is contained in:
@@ -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()),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user