refactor: separate expression metadata and tests from numerical identity
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
//! Indicator metadata is versioned independently from the numerical kernel.
|
||||||
|
use crate::factor_events::{CONTRACT, OPERATORS, TA_REV};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use ta_lib::abstract_api::{self, OptInputType};
|
||||||
|
|
||||||
|
pub fn catalog() -> Value {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let mut implementation = Sha256::new();
|
||||||
|
for file in [include_bytes!("factor_events.rs").as_slice(), include_bytes!("factor_cross_section.rs").as_slice(),
|
||||||
|
include_bytes!("daily_patterns.rs").as_slice(),include_bytes!("market_event_context.rs").as_slice(),
|
||||||
|
include_bytes!("session_events.rs").as_slice(),include_bytes!("pattern_context.rs").as_slice(),TA_REV.as_bytes()] {implementation.update(file);}
|
||||||
|
let implementation_sha256=format!("{:x}",implementation.finalize());
|
||||||
|
let indicators: Vec<Value> = abstract_api::funcs().map(|f| json!({
|
||||||
|
"name":f.name, "group":format!("{:?}",f.group), "description":f.hint,
|
||||||
|
"inputs":f.inputs.iter().map(|p|json!({"name":p.param_name,"kind":format!("{:?}",p.kind),"flags":p.flags.0})).collect::<Vec<_>>(),
|
||||||
|
"parameters":f.opt_inputs.iter().map(|p|json!({"name":p.param_name,"label":p.display_name,"description":p.hint,"domain":parameter_domain(p.kind)})).collect::<Vec<_>>(),
|
||||||
|
"outputs":f.outputs.iter().enumerate().map(|(i,p)|json!({"index":i,"name":p.param_name,"kind":format!("{:?}",p.kind)})).collect::<Vec<_>>(),
|
||||||
|
"unstable_period":format!("{:?}",f.unst_id), "production_eligible":false,
|
||||||
|
})).collect();
|
||||||
|
json!({"contract":CONTRACT,"parameter_domain_contract":"fidc.indicator-parameter-domain/v1","expression_kernel_sha256":implementation_sha256,"library":{"name":"TA-Lib native Rust","revision":TA_REV,"license":"BSD-3-Clause"},
|
||||||
|
"execution_context_contract":crate::pattern_context::CONTRACT,
|
||||||
|
"execution_context_fields":crate::pattern_context::CONTEXT_FIELDS,
|
||||||
|
"market_event_context_contract":crate::market_event_context::CONTRACT,
|
||||||
|
"market_event_kernel_sha256":crate::market_event_context::implementation_sha256(),
|
||||||
|
"market_event_common_fields":crate::market_event_context::COMMON_FIELDS,
|
||||||
|
"market_event_industry_fields":crate::market_event_context::INDUSTRY_FIELDS,
|
||||||
|
"session_events":crate::session_events::EVENTS,"session_event_contract":crate::session_events::CONTRACT,
|
||||||
|
"indicators":indicators,"operators":OPERATORS,"cross_section_operators":crate::factor_cross_section::OPERATORS,"read_only":true,"live_routing":false,
|
||||||
|
"policies":{"null":"unknown_not_false","warmup":"null_until_full_history","recursive_seed":"frozen_input_start",
|
||||||
|
"breakout":"previous_window_excludes_current","boolean":"three_valued_logic","daily_execution":"next_completed_session",
|
||||||
|
"minute_execution":"strictly_after_completed_bar","cross_section":"requires_separate_complete_universe_contract"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parameter_domain(kind: OptInputType) -> Value {
|
||||||
|
match kind {
|
||||||
|
OptInputType::IntegerRange { min, max, default, .. } => json!({
|
||||||
|
"value_type":"integer", "minimum":min, "maximum":max, "default":default,
|
||||||
|
}),
|
||||||
|
OptInputType::RealRange { min, max, default, precision, .. } => json!({
|
||||||
|
"value_type":"number", "minimum":min, "maximum":max,
|
||||||
|
"default":default, "display_precision":precision,
|
||||||
|
}),
|
||||||
|
OptInputType::IntegerList { values, default } => json!({
|
||||||
|
"value_type":"integer", "default":default,
|
||||||
|
"choices":values.iter().map(|(value,label)|json!({"value":value,"label":label})).collect::<Vec<_>>(),
|
||||||
|
}),
|
||||||
|
OptInputType::RealList { values, default } => json!({
|
||||||
|
"value_type":"number", "default":default,
|
||||||
|
"choices":values.iter().map(|(value,label)|json!({"value":value,"label":label})).collect::<Vec<_>>(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Causal, typed indicator/event expressions shared by research and trading.
|
//! Causal, typed indicator/event expressions shared by research and trading.
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use ta_lib::{
|
use ta_lib::{
|
||||||
Core,
|
Core,
|
||||||
@@ -82,7 +82,7 @@ pub struct Series {
|
|||||||
pub values: Vec<Option<f64>>,
|
pub values: Vec<Option<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const OPERATORS: &[&str] = &[
|
pub(crate) const OPERATORS: &[&str] = &[
|
||||||
"GT",
|
"GT",
|
||||||
"GTE",
|
"GTE",
|
||||||
"LT",
|
"LT",
|
||||||
@@ -168,53 +168,7 @@ const OPERATORS: &[&str] = &[
|
|||||||
"IF",
|
"IF",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn catalog() -> Value {
|
pub use crate::factor_event_catalog::catalog;
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
let mut implementation = Sha256::new();
|
|
||||||
for file in [include_bytes!("factor_events.rs").as_slice(), include_bytes!("factor_cross_section.rs").as_slice(),
|
|
||||||
include_bytes!("daily_patterns.rs").as_slice(),include_bytes!("market_event_context.rs").as_slice(),
|
|
||||||
include_bytes!("session_events.rs").as_slice(),include_bytes!("pattern_context.rs").as_slice(),TA_REV.as_bytes()] {implementation.update(file);}
|
|
||||||
let implementation_sha256=format!("{:x}",implementation.finalize());
|
|
||||||
let indicators: Vec<Value> = abstract_api::funcs().map(|f| json!({
|
|
||||||
"name":f.name, "group":format!("{:?}",f.group), "description":f.hint,
|
|
||||||
"inputs":f.inputs.iter().map(|p|json!({"name":p.param_name,"kind":format!("{:?}",p.kind),"flags":p.flags.0})).collect::<Vec<_>>(),
|
|
||||||
"parameters":f.opt_inputs.iter().map(|p|json!({"name":p.param_name,"label":p.display_name,"description":p.hint,"domain":parameter_domain(p.kind)})).collect::<Vec<_>>(),
|
|
||||||
"outputs":f.outputs.iter().enumerate().map(|(i,p)|json!({"index":i,"name":p.param_name,"kind":format!("{:?}",p.kind)})).collect::<Vec<_>>(),
|
|
||||||
"unstable_period":format!("{:?}",f.unst_id), "production_eligible":false,
|
|
||||||
})).collect();
|
|
||||||
json!({"contract":CONTRACT,"parameter_domain_contract":"fidc.indicator-parameter-domain/v1","expression_kernel_sha256":implementation_sha256,"library":{"name":"TA-Lib native Rust","revision":TA_REV,"license":"BSD-3-Clause"},
|
|
||||||
"execution_context_contract":crate::pattern_context::CONTRACT,
|
|
||||||
"execution_context_fields":crate::pattern_context::CONTEXT_FIELDS,
|
|
||||||
"market_event_context_contract":crate::market_event_context::CONTRACT,
|
|
||||||
"market_event_kernel_sha256":crate::market_event_context::implementation_sha256(),
|
|
||||||
"market_event_common_fields":crate::market_event_context::COMMON_FIELDS,
|
|
||||||
"market_event_industry_fields":crate::market_event_context::INDUSTRY_FIELDS,
|
|
||||||
"session_events":crate::session_events::EVENTS,"session_event_contract":crate::session_events::CONTRACT,
|
|
||||||
"indicators":indicators,"operators":OPERATORS,"cross_section_operators":crate::factor_cross_section::OPERATORS,"read_only":true,"live_routing":false,
|
|
||||||
"policies":{"null":"unknown_not_false","warmup":"null_until_full_history","recursive_seed":"frozen_input_start",
|
|
||||||
"breakout":"previous_window_excludes_current","boolean":"three_valued_logic","daily_execution":"next_completed_session",
|
|
||||||
"minute_execution":"strictly_after_completed_bar","cross_section":"requires_separate_complete_universe_contract"}})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameter_domain(kind: OptInputType) -> Value {
|
|
||||||
match kind {
|
|
||||||
OptInputType::IntegerRange { min, max, default, .. } => json!({
|
|
||||||
"value_type":"integer", "minimum":min, "maximum":max, "default":default,
|
|
||||||
}),
|
|
||||||
OptInputType::RealRange { min, max, default, precision, .. } => json!({
|
|
||||||
"value_type":"number", "minimum":min, "maximum":max,
|
|
||||||
"default":default, "display_precision":precision,
|
|
||||||
}),
|
|
||||||
OptInputType::IntegerList { values, default } => json!({
|
|
||||||
"value_type":"integer", "default":default,
|
|
||||||
"choices":values.iter().map(|(value,label)|json!({"value":value,"label":label})).collect::<Vec<_>>(),
|
|
||||||
}),
|
|
||||||
OptInputType::RealList { values, default } => json!({
|
|
||||||
"value_type":"number", "default":default,
|
|
||||||
"choices":values.iter().map(|(value,label)|json!({"value":value,"label":label})).collect::<Vec<_>>(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Frame {
|
impl Frame {
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
@@ -978,164 +932,5 @@ fn operator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "factor_events_tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn every_parameter_domain_is_structured_and_matches_native_defaults() {
|
|
||||||
for function in abstract_api::funcs() {
|
|
||||||
let handle = abstract_api::get_func_handle(function.name).unwrap();
|
|
||||||
let core = Core::new();
|
|
||||||
let mut call = handle.new_call(&core);
|
|
||||||
for (index, parameter) in function.opt_inputs.iter().enumerate() {
|
|
||||||
let domain = parameter_domain(parameter.kind);
|
|
||||||
let default = domain["default"].as_f64().unwrap();
|
|
||||||
assert!(default.is_finite(), "{} {}", function.name, parameter.param_name);
|
|
||||||
if let Some(choices) = domain.get("choices") {
|
|
||||||
assert!(choices.as_array().unwrap().iter().any(|v| v["value"].as_f64() == Some(default)));
|
|
||||||
} else {
|
|
||||||
assert!(default >= domain["minimum"].as_f64().unwrap());
|
|
||||||
assert!(default <= domain["maximum"].as_f64().unwrap());
|
|
||||||
}
|
|
||||||
if domain["value_type"] == "integer" {
|
|
||||||
assert_eq!(default.fract(), 0.0);
|
|
||||||
call.set_opt(index, default as i32).unwrap();
|
|
||||||
} else {
|
|
||||||
call.set_opt(index, default).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(call.lookback().is_ok(), "{}", function.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parameter_domains_keep_enumeration_labels_without_debug_string_parsing() {
|
|
||||||
let catalog = catalog();
|
|
||||||
assert_eq!(catalog["parameter_domain_contract"], "fidc.indicator-parameter-domain/v1");
|
|
||||||
let indicators = catalog["indicators"].as_array().unwrap();
|
|
||||||
let rsi = indicators.iter().find(|v| v["name"] == "RSI").unwrap();
|
|
||||||
assert_eq!(rsi["parameters"][0]["domain"]["minimum"], 2);
|
|
||||||
let stoch = indicators.iter().find(|v| v["name"] == "STOCH").unwrap();
|
|
||||||
let ma_type = stoch["parameters"].as_array().unwrap().iter().find(|p| p["name"] == "optInSlowK_MAType").unwrap();
|
|
||||||
assert!(ma_type["domain"]["choices"].as_array().unwrap().iter().any(|v| v["label"] == "EMA" && v["value"] == 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn frame(values: Vec<Option<f64>>) -> Frame {
|
|
||||||
let start = DateTime::parse_from_rfc3339("2026-09-01T15:30:00+08:00").unwrap();
|
|
||||||
let times = (0..values.len())
|
|
||||||
.map(|i| start + chrono::Duration::days(i as i64))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Frame {
|
|
||||||
symbol: "TEST".into(),
|
|
||||||
frequency: "1d".into(),
|
|
||||||
decision_at: *times.last().unwrap(),
|
|
||||||
available_at: times.clone(),
|
|
||||||
timestamps: times,
|
|
||||||
fields: BTreeMap::from([("close".into(), values)]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn expr(v: Value) -> Expr {
|
|
||||||
serde_json::from_value(v).unwrap()
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn ta_sma_real_values_and_parameter_validation() {
|
|
||||||
let frame = frame(vec![Some(1.0), Some(2.0), Some(3.0), Some(4.0)]);
|
|
||||||
let e = expr(
|
|
||||||
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"optInTimePeriod":3}}),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
evaluate(&e, &frame).unwrap().values,
|
|
||||||
vec![None, None, Some(2.0), Some(3.0)]
|
|
||||||
);
|
|
||||||
let bad = expr(
|
|
||||||
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"period":3}}),
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
evaluate(&bad, &frame)
|
|
||||||
.unwrap_err()
|
|
||||||
.contains("parameter_unknown")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn cross_is_event_not_state_and_never_uses_future() {
|
|
||||||
let f = frame(vec![
|
|
||||||
Some(9.0),
|
|
||||||
Some(10.0),
|
|
||||||
Some(11.0),
|
|
||||||
Some(12.0),
|
|
||||||
Some(8.0),
|
|
||||||
]);
|
|
||||||
let e = expr(
|
|
||||||
json!({"kind":"operator","name":"CROSS_ABOVE","args":[{"kind":"field","name":"close"},{"kind":"number","value":10.0}]}),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
evaluate(&e, &f).unwrap().values,
|
|
||||||
vec![None, Some(0.0), Some(1.0), Some(0.0), Some(0.0)]
|
|
||||||
);
|
|
||||||
let mut invalid = f.clone();
|
|
||||||
invalid.available_at[4] = invalid.decision_at + chrono::Duration::seconds(1);
|
|
||||||
assert!(evaluate(&e, &invalid).is_err());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn missing_is_not_zero_and_breakout_excludes_current() {
|
|
||||||
let f = frame(vec![Some(1.0), Some(2.0), Some(3.0), None, Some(5.0)]);
|
|
||||||
let e = expr(
|
|
||||||
json!({"kind":"operator","name":"BREAK_HIGH","window":2,"args":[{"kind":"field","name":"close"}]}),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
evaluate(&e, &f).unwrap().values,
|
|
||||||
vec![None, None, Some(1.0), None, None]
|
|
||||||
);
|
|
||||||
let zero = expr(
|
|
||||||
json!({"kind":"operator","name":"DIV","args":[{"kind":"field","name":"close"},{"kind":"number","value":0}]}),
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
evaluate(&zero, &f)
|
|
||||||
.unwrap()
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.all(Option::is_none)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn ta_rewarms_after_gap_and_const_zscore_is_unknown() {
|
|
||||||
let f = frame(vec![Some(1.0), Some(1.0), None, Some(2.0), Some(2.0)]);
|
|
||||||
let e = expr(
|
|
||||||
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"optInTimePeriod":2}}),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
evaluate(&e, &f).unwrap().values,
|
|
||||||
vec![None, Some(1.0), None, None, Some(2.0)]
|
|
||||||
);
|
|
||||||
let e = expr(
|
|
||||||
json!({"kind":"operator","name":"ZSCORE","window":2,"args":[{"kind":"field","name":"close"}]}),
|
|
||||||
);
|
|
||||||
assert!(evaluate(&e, &f).unwrap().values.iter().all(Option::is_none));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn no_event_has_no_bars_since_and_type_errors_reject() {
|
|
||||||
let f = frame(vec![Some(1.0), Some(1.0), Some(1.0)]);
|
|
||||||
let state = json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":5}]});
|
|
||||||
let e = expr(json!({"kind":"operator","name":"BARS_SINCE","args":[state]}));
|
|
||||||
assert!(evaluate(&e, &f).unwrap().values.iter().all(Option::is_none));
|
|
||||||
assert!(
|
|
||||||
evaluate(
|
|
||||||
&expr(
|
|
||||||
json!({"kind":"operator","name":"NOT","args":[{"kind":"field","name":"close"}]})
|
|
||||||
),
|
|
||||||
&f
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn literal_unknown_fields_reject_and_catalog_is_not_trading_permission() {
|
|
||||||
assert!(
|
|
||||||
serde_json::from_value::<Expr>(json!({"kind":"number","value":1,"account_id":2}))
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
let c = catalog();
|
|
||||||
assert!(c["indicators"].as_array().unwrap().len() > 190);
|
|
||||||
assert_eq!(c["live_routing"], false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use crate::factor_event_catalog::parameter_domain;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_parameter_domain_is_structured_and_matches_native_defaults() {
|
||||||
|
for function in abstract_api::funcs() {
|
||||||
|
let handle = abstract_api::get_func_handle(function.name).unwrap();
|
||||||
|
let core = Core::new();
|
||||||
|
let mut call = handle.new_call(&core);
|
||||||
|
for (index, parameter) in function.opt_inputs.iter().enumerate() {
|
||||||
|
let domain = parameter_domain(parameter.kind);
|
||||||
|
let default = domain["default"].as_f64().unwrap();
|
||||||
|
assert!(default.is_finite(), "{} {}", function.name, parameter.param_name);
|
||||||
|
if let Some(choices) = domain.get("choices") {
|
||||||
|
assert!(choices.as_array().unwrap().iter().any(|v| v["value"].as_f64() == Some(default)));
|
||||||
|
} else {
|
||||||
|
assert!(default >= domain["minimum"].as_f64().unwrap());
|
||||||
|
assert!(default <= domain["maximum"].as_f64().unwrap());
|
||||||
|
}
|
||||||
|
if domain["value_type"] == "integer" {
|
||||||
|
assert_eq!(default.fract(), 0.0);
|
||||||
|
call.set_opt(index, default as i32).unwrap();
|
||||||
|
} else {
|
||||||
|
call.set_opt(index, default).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(call.lookback().is_ok(), "{}", function.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_domains_keep_enumeration_labels_without_debug_string_parsing() {
|
||||||
|
let catalog = catalog();
|
||||||
|
assert_eq!(catalog["parameter_domain_contract"], "fidc.indicator-parameter-domain/v1");
|
||||||
|
let indicators = catalog["indicators"].as_array().unwrap();
|
||||||
|
let rsi = indicators.iter().find(|v| v["name"] == "RSI").unwrap();
|
||||||
|
assert_eq!(rsi["parameters"][0]["domain"]["minimum"], 2);
|
||||||
|
let stoch = indicators.iter().find(|v| v["name"] == "STOCH").unwrap();
|
||||||
|
let ma_type = stoch["parameters"].as_array().unwrap().iter().find(|p| p["name"] == "optInSlowK_MAType").unwrap();
|
||||||
|
assert!(ma_type["domain"]["choices"].as_array().unwrap().iter().any(|v| v["label"] == "EMA" && v["value"] == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(values: Vec<Option<f64>>) -> Frame {
|
||||||
|
let start = DateTime::parse_from_rfc3339("2026-09-01T15:30:00+08:00").unwrap();
|
||||||
|
let times = (0..values.len())
|
||||||
|
.map(|i| start + chrono::Duration::days(i as i64))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Frame {
|
||||||
|
symbol: "TEST".into(),
|
||||||
|
frequency: "1d".into(),
|
||||||
|
decision_at: *times.last().unwrap(),
|
||||||
|
available_at: times.clone(),
|
||||||
|
timestamps: times,
|
||||||
|
fields: BTreeMap::from([("close".into(), values)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn expr(v: Value) -> Expr {
|
||||||
|
serde_json::from_value(v).unwrap()
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn ta_sma_real_values_and_parameter_validation() {
|
||||||
|
let frame = frame(vec![Some(1.0), Some(2.0), Some(3.0), Some(4.0)]);
|
||||||
|
let e = expr(
|
||||||
|
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"optInTimePeriod":3}}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate(&e, &frame).unwrap().values,
|
||||||
|
vec![None, None, Some(2.0), Some(3.0)]
|
||||||
|
);
|
||||||
|
let bad = expr(
|
||||||
|
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"period":3}}),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
evaluate(&bad, &frame)
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("parameter_unknown")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn cross_is_event_not_state_and_never_uses_future() {
|
||||||
|
let f = frame(vec![
|
||||||
|
Some(9.0),
|
||||||
|
Some(10.0),
|
||||||
|
Some(11.0),
|
||||||
|
Some(12.0),
|
||||||
|
Some(8.0),
|
||||||
|
]);
|
||||||
|
let e = expr(
|
||||||
|
json!({"kind":"operator","name":"CROSS_ABOVE","args":[{"kind":"field","name":"close"},{"kind":"number","value":10.0}]}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate(&e, &f).unwrap().values,
|
||||||
|
vec![None, Some(0.0), Some(1.0), Some(0.0), Some(0.0)]
|
||||||
|
);
|
||||||
|
let mut invalid = f.clone();
|
||||||
|
invalid.available_at[4] = invalid.decision_at + chrono::Duration::seconds(1);
|
||||||
|
assert!(evaluate(&e, &invalid).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn missing_is_not_zero_and_breakout_excludes_current() {
|
||||||
|
let f = frame(vec![Some(1.0), Some(2.0), Some(3.0), None, Some(5.0)]);
|
||||||
|
let e = expr(
|
||||||
|
json!({"kind":"operator","name":"BREAK_HIGH","window":2,"args":[{"kind":"field","name":"close"}]}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate(&e, &f).unwrap().values,
|
||||||
|
vec![None, None, Some(1.0), None, None]
|
||||||
|
);
|
||||||
|
let zero = expr(
|
||||||
|
json!({"kind":"operator","name":"DIV","args":[{"kind":"field","name":"close"},{"kind":"number","value":0}]}),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
evaluate(&zero, &f)
|
||||||
|
.unwrap()
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.all(Option::is_none)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn ta_rewarms_after_gap_and_const_zscore_is_unknown() {
|
||||||
|
let f = frame(vec![Some(1.0), Some(1.0), None, Some(2.0), Some(2.0)]);
|
||||||
|
let e = expr(
|
||||||
|
json!({"kind":"indicator","name":"SMA","inputs":[{"kind":"field","name":"close"}],"parameters":{"optInTimePeriod":2}}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evaluate(&e, &f).unwrap().values,
|
||||||
|
vec![None, Some(1.0), None, None, Some(2.0)]
|
||||||
|
);
|
||||||
|
let e = expr(
|
||||||
|
json!({"kind":"operator","name":"ZSCORE","window":2,"args":[{"kind":"field","name":"close"}]}),
|
||||||
|
);
|
||||||
|
assert!(evaluate(&e, &f).unwrap().values.iter().all(Option::is_none));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn no_event_has_no_bars_since_and_type_errors_reject() {
|
||||||
|
let f = frame(vec![Some(1.0), Some(1.0), Some(1.0)]);
|
||||||
|
let state = json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":5}]});
|
||||||
|
let e = expr(json!({"kind":"operator","name":"BARS_SINCE","args":[state]}));
|
||||||
|
assert!(evaluate(&e, &f).unwrap().values.iter().all(Option::is_none));
|
||||||
|
assert!(
|
||||||
|
evaluate(
|
||||||
|
&expr(
|
||||||
|
json!({"kind":"operator","name":"NOT","args":[{"kind":"field","name":"close"}]})
|
||||||
|
),
|
||||||
|
&f
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn literal_unknown_fields_reject_and_catalog_is_not_trading_permission() {
|
||||||
|
assert!(
|
||||||
|
serde_json::from_value::<Expr>(json!({"kind":"number","value":1,"account_id":2}))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let c = catalog();
|
||||||
|
assert!(c["indicators"].as_array().unwrap().len() > 190);
|
||||||
|
assert_eq!(c["live_routing"], false);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ pub mod daily_patterns;
|
|||||||
pub mod pattern_context;
|
pub mod pattern_context;
|
||||||
pub mod session_events;
|
pub mod session_events;
|
||||||
pub mod factor_events;
|
pub mod factor_events;
|
||||||
|
mod factor_event_catalog;
|
||||||
pub mod factor_cross_section;
|
pub mod factor_cross_section;
|
||||||
pub mod market_event_context;
|
pub mod market_event_context;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
|
|||||||
Reference in New Issue
Block a user