研究计算隔离证券数据异常并保持交易严格校验
This commit is contained in:
@@ -233,7 +233,7 @@ pub(crate) fn evaluate_with_context(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|b| (b.date, b))
|
.map(|b| (b.date, b))
|
||||||
.collect::<BTreeMap<_, _>>();
|
.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!(
|
return Err(format!(
|
||||||
"pattern_input_invalid: symbol={}, reason=duplicate_or_out_of_scope",
|
"pattern_input_invalid: symbol={}, reason=duplicate_or_out_of_scope",
|
||||||
series.symbol
|
series.symbol
|
||||||
@@ -393,7 +393,7 @@ pub(crate) fn evaluate_with_context(
|
|||||||
)?;
|
)?;
|
||||||
if value < 0.0 || (name == "prev_close" && value == 0.0) {
|
if value < 0.0 || (name == "prev_close" && value == 0.0) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"pattern_input_invalid: {} {d} {name}",
|
"pattern_input_invalid: symbol={}, date={d}, field={name}, reason=invalid_value",
|
||||||
series.symbol
|
series.symbol
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -674,6 +674,13 @@ pub fn evaluate_batch(
|
|||||||
spec: PatternSpec,
|
spec: PatternSpec,
|
||||||
days: &[NaiveDate],
|
days: &[NaiveDate],
|
||||||
series: &[PatternSeries],
|
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> {
|
) -> Result<Value, String> {
|
||||||
let spec = spec.validate()?;
|
let spec = spec.validate()?;
|
||||||
if series.is_empty()
|
if series.is_empty()
|
||||||
@@ -689,13 +696,26 @@ pub fn evaluate_batch(
|
|||||||
}
|
}
|
||||||
let rows = series
|
let rows = series
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| evaluate(&spec, days, s))
|
.map(|s| research_row(evaluate(&spec, days, s), s, isolate_data_errors))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
Ok(
|
Ok(
|
||||||
json!({"contract":CONTRACT,"spec":spec,"required_history":spec.history_len(),"rows":rows,"read_only":true}),
|
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.
|
/// Values are supplied only by the verified research transport or dataset context builder.
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -709,6 +729,13 @@ pub struct ResearchContext {
|
|||||||
pub fn evaluate_research_batch(
|
pub fn evaluate_research_batch(
|
||||||
spec: PatternSpec, days: &[NaiveDate], series: &[PatternSeries],
|
spec: PatternSpec, days: &[NaiveDate], series: &[PatternSeries],
|
||||||
context: &ResearchContext, numeric_output: bool,
|
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> {
|
) -> Result<Value, String> {
|
||||||
let common_fields = ["index_open", "index_high", "index_low", "index_close"];
|
let common_fields = ["index_open", "index_high", "index_low", "index_close"];
|
||||||
let symbol_fields = ["scope_rank", "scope_percentile", "scope_size"];
|
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));
|
return Err(format!("research_context_invalid: {} {name}", item.symbol));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut result = evaluate_with_context(&spec, days, item, &fields, numeric_output)?;
|
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<_,_>>());
|
result["values"]["research_context_latest"] = json!(fields.iter().map(|(k,v)|(k,v.last().copied().flatten())).collect::<BTreeMap<_,_>>());
|
||||||
rows.push(result);
|
rows.push(result);
|
||||||
}
|
}
|
||||||
Ok(json!({"contract":CONTRACT,"context_contract":"fidc_research_event_context_v1","spec":spec,
|
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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[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() {
|
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 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},
|
let spec:PatternSpec=serde_json::from_value(json!({"template":"expression","parameters":{"history_window":3},
|
||||||
|
|||||||
Reference in New Issue
Block a user