diff --git a/crates/fidc-core/src/factor_events.rs b/crates/fidc-core/src/factor_events.rs index 5844c0a..d841eb6 100644 --- a/crates/fidc-core/src/factor_events.rs +++ b/crates/fidc-core/src/factor_events.rs @@ -178,11 +178,11 @@ pub fn catalog() -> Value { let indicators: Vec = 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::>(), - "parameters":f.opt_inputs.iter().map(|p|json!({"name":p.param_name,"label":p.display_name,"description":p.hint,"domain":format!("{:?}",p.kind)})).collect::>(), + "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::>(), "outputs":f.outputs.iter().enumerate().map(|(i,p)|json!({"index":i,"name":p.param_name,"kind":format!("{:?}",p.kind)})).collect::>(), "unstable_period":format!("{:?}",f.unst_id), "production_eligible":false, })).collect(); - json!({"contract":CONTRACT,"expression_kernel_sha256":implementation_sha256,"library":{"name":"TA-Lib native Rust","revision":TA_REV,"license":"BSD-3-Clause"}, + 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, @@ -196,6 +196,26 @@ pub fn catalog() -> Value { "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::>(), + }), + OptInputType::RealList { values, default } => json!({ + "value_type":"number", "default":default, + "choices":values.iter().map(|(value,label)|json!({"value":value,"label":label})).collect::>(), + }), + } +} + impl Frame { pub fn validate(&self) -> Result<(), String> { let n = self.timestamps.len(); @@ -960,6 +980,46 @@ fn operator( #[cfg(test)] mod tests { use super::*; + + #[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>) -> Frame { let start = DateTime::parse_from_rfc3339("2026-09-01T15:30:00+08:00").unwrap(); let times = (0..values.len())