研究计算隔离证券数据异常并保持交易严格校验
This commit is contained in:
@@ -233,7 +233,7 @@ pub(crate) fn evaluate_with_context(
|
||||
.iter()
|
||||
.map(|b| (b.date, b))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
if by_day.len() != series.bars.len() || series.bars.iter().any(|b| !days.contains(&b.date)) {
|
||||
if by_day.len() != series.bars.len() || series.bars.iter().any(|b| days.binary_search(&b.date).is_err()) {
|
||||
return Err(format!(
|
||||
"pattern_input_invalid: symbol={}, reason=duplicate_or_out_of_scope",
|
||||
series.symbol
|
||||
@@ -393,7 +393,7 @@ pub(crate) fn evaluate_with_context(
|
||||
)?;
|
||||
if value < 0.0 || (name == "prev_close" && value == 0.0) {
|
||||
return Err(format!(
|
||||
"pattern_input_invalid: {} {d} {name}",
|
||||
"pattern_input_invalid: symbol={}, date={d}, field={name}, reason=invalid_value",
|
||||
series.symbol
|
||||
));
|
||||
}
|
||||
@@ -674,6 +674,13 @@ pub fn evaluate_batch(
|
||||
spec: PatternSpec,
|
||||
days: &[NaiveDate],
|
||||
series: &[PatternSeries],
|
||||
) -> Result<Value, String> {
|
||||
evaluate_batch_with_policy(spec, days, series, false)
|
||||
}
|
||||
|
||||
/// Partial results are research diagnostics, never strategy execution inputs.
|
||||
pub fn evaluate_batch_with_policy(
|
||||
spec: PatternSpec, days: &[NaiveDate], series: &[PatternSeries], isolate_data_errors: bool,
|
||||
) -> Result<Value, String> {
|
||||
let spec = spec.validate()?;
|
||||
if series.is_empty()
|
||||
@@ -689,13 +696,26 @@ pub fn evaluate_batch(
|
||||
}
|
||||
let rows = series
|
||||
.iter()
|
||||
.map(|s| evaluate(&spec, days, s))
|
||||
.map(|s| research_row(evaluate(&spec, days, s), s, isolate_data_errors))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(
|
||||
json!({"contract":CONTRACT,"spec":spec,"required_history":spec.history_len(),"rows":rows,"read_only":true}),
|
||||
)
|
||||
}
|
||||
|
||||
fn research_row(result: Result<PatternResult, String>, series: &PatternSeries, isolate: bool) -> Result<Value, String> {
|
||||
match result {
|
||||
Ok(row) => Ok(json!(row)),
|
||||
Err(detail) if isolate && detail.starts_with(&format!("pattern_input_invalid: symbol={},", series.symbol)) => {
|
||||
let fields = detail.split(", ").filter_map(|p| p.split_once('=')).collect::<BTreeMap<_,_>>();
|
||||
Ok(json!({"symbol":series.symbol,"name":series.name,"matched":null,"score":null,
|
||||
"checks":[],"values":{},"anchor":null,"exclusion":null,
|
||||
"data_issue":{"reason":fields.get("reason"),"date":fields.get("date"),"field":fields.get("field"),"detail":detail}}))
|
||||
},
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Values are supplied only by the verified research transport or dataset context builder.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -709,6 +729,13 @@ pub struct ResearchContext {
|
||||
pub fn evaluate_research_batch(
|
||||
spec: PatternSpec, days: &[NaiveDate], series: &[PatternSeries],
|
||||
context: &ResearchContext, numeric_output: bool,
|
||||
) -> Result<Value, String> {
|
||||
evaluate_research_batch_with_policy(spec, days, series, context, numeric_output, false)
|
||||
}
|
||||
|
||||
pub fn evaluate_research_batch_with_policy(
|
||||
spec: PatternSpec, days: &[NaiveDate], series: &[PatternSeries],
|
||||
context: &ResearchContext, numeric_output: bool, isolate_data_errors: bool,
|
||||
) -> Result<Value, String> {
|
||||
let common_fields = ["index_open", "index_high", "index_low", "index_close"];
|
||||
let symbol_fields = ["scope_rank", "scope_percentile", "scope_size"];
|
||||
@@ -741,8 +768,8 @@ pub fn evaluate_research_batch(
|
||||
return Err(format!("research_context_invalid: {} {name}", item.symbol));
|
||||
}
|
||||
}
|
||||
let mut result = evaluate_with_context(&spec, days, item, &fields, numeric_output)?;
|
||||
result.values["research_context_latest"] = json!(fields.iter().map(|(k,v)|(k,v.last().copied().flatten())).collect::<BTreeMap<_,_>>());
|
||||
let mut result = research_row(evaluate_with_context(&spec, days, item, &fields, numeric_output), item, isolate_data_errors)?;
|
||||
result["values"]["research_context_latest"] = json!(fields.iter().map(|(k,v)|(k,v.last().copied().flatten())).collect::<BTreeMap<_,_>>());
|
||||
rows.push(result);
|
||||
}
|
||||
Ok(json!({"contract":CONTRACT,"context_contract":"fidc_research_event_context_v1","spec":spec,
|
||||
@@ -786,6 +813,22 @@ pub fn expression_specs(expression: &str) -> Result<Vec<PatternSpec>, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn research_isolates_missing_listing_day_without_weakening_execution() {
|
||||
let days = ["2026-06-11", "2026-06-12"].map(|d|d.parse::<NaiveDate>().unwrap());
|
||||
let spec: PatternSpec = serde_json::from_value(json!({"template":"ma_below","parameters":{"ma_window":2}})).unwrap();
|
||||
let make = |symbol: &str| -> PatternSeries { serde_json::from_value(json!({"symbol":symbol,"listed_at":"2026-06-11","bars":days.map(|d|json!({"date":d,"open":10.,"high":11.,"low":9.,"close":10.,"volume":100.,"adjustment_factor_backward1":1.,"paused":false,"source_path":"/controlled/source.parquet"}))})).unwrap() };
|
||||
let complete=make("300395.SZ");let mut missing=make("920083.BJ");missing.bars.remove(0);
|
||||
let members=[complete.clone(),missing];
|
||||
assert!(evaluate_batch(spec.clone(),&days,&members).unwrap_err().contains("missing_market_row"));
|
||||
let partial=evaluate_batch_with_policy(spec.clone(),&days,&members,true).unwrap();
|
||||
assert_eq!(partial["rows"][0],json!(evaluate(&spec.validate().unwrap(),&days,&complete).unwrap()));
|
||||
assert!(partial["rows"][1]["matched"].is_null());
|
||||
assert_eq!(partial["rows"][1]["data_issue"]["date"],"2026-06-11");
|
||||
assert_eq!(partial["rows"][1]["data_issue"]["reason"],"missing_market_row");
|
||||
let invalid:PatternSpec=serde_json::from_value(json!({"template":"not-a-template"})).unwrap();
|
||||
assert!(evaluate_batch_with_policy(invalid,&days,&members,true).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn research_index_and_ranking_context_never_unlock_strategy_mapping() {
|
||||
let days=["2026-09-04","2026-09-07","2026-09-08"].map(|s|s.parse::<NaiveDate>().unwrap());
|
||||
let spec:PatternSpec=serde_json::from_value(json!({"template":"expression","parameters":{"history_window":3},
|
||||
|
||||
Reference in New Issue
Block a user