统一日线事件上下文并接入完成分钟事件回测
This commit is contained in:
@@ -10,6 +10,7 @@ pub const CONTRACT: &str = "fidc_daily_ohlcv_pattern_v1";
|
||||
pub fn catalog() -> Value {
|
||||
json!({"contract":CONTRACT,"templates":{
|
||||
"expression":{"label":"指标与事件条件","parameters":{"history_window":[300,2,3000]},"stages":["selection","buy","sell","position_management"],"method":"冻结历史窗口与表达式;预热不足或未定义值不产生信号。复用共享指标事件内核,不修改既有任务。"},
|
||||
"session_event":{"label":"已完成分钟事件","parameters":{"opening_minutes":[30,1,120],"volume_window":[5,2,120],"volume_multiple":[3.0,1,20]},"stages":["selection","buy","sell"],"method":"仅本交易日完整分钟OHLCVA,信号K线必须早于执行时点;不使用盘口快照伪造K线。"},
|
||||
"strength":{"label":"趋势强势","parameters":{"momentum_window":[25,5,120],"fast_window":[20,2,60],"slow_window":[60,20,252]},"stages":["selection","buy"],"method":"收盘价>短均线>长均线,按区间动量排序;不是当日金叉。"},
|
||||
"breakout":{"label":"前高突破","parameters":{"high_window":[60,5,252],"volume_window":[10,2,60],"volume_multiple":[1.3,1,10],"max_upper_shadow":[0.1,0,1]},"stages":["selection","buy"],"method":"收盘突破此前N日最高价,量达到此前M日均量倍数,上影比例受限;参考窗口不含当日。"},
|
||||
"volume_spike":{"label":"放量上涨","parameters":{"volume_window":[5,2,60],"volume_multiple":[3.0,1,10]},"stages":["selection","buy"],"method":"当日上涨且量达到此前N日最大量的指定倍数;不等同价格创新高。"},
|
||||
@@ -31,10 +32,27 @@ pub struct PatternSpec {
|
||||
pub parameters: BTreeMap<String, Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub expression: Option<crate::factor_events::Expr>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub execution_context: Option<crate::pattern_context::ExecutionContext>,
|
||||
#[serde(default,skip_serializing_if="Option::is_none")]
|
||||
pub session_event:Option<String>,
|
||||
}
|
||||
impl PatternSpec {
|
||||
pub fn validate(self) -> Result<Self, String> {
|
||||
self.validate_with_context(&[])
|
||||
if self.template=="session_event" {
|
||||
if !self.session_event.as_deref().is_some_and(|id|crate::session_events::EVENTS.contains(&id)) || self.execution_context.is_some() {return Err("session_event_contract_invalid".into());}
|
||||
} else if self.session_event.is_some() {return Err("unexpected_session_event_id".into());}
|
||||
let allowed = if let Some(context) = &self.execution_context {
|
||||
context.validate(self.expression.as_ref().ok_or("pattern_context_requires_expression")?)?;
|
||||
crate::pattern_context::CONTEXT_FIELDS
|
||||
} else { &[] };
|
||||
let spec = self.validate_with_context(allowed)?;
|
||||
if let Some(context) = &spec.execution_context {
|
||||
if context.rank_universe.len().saturating_mul(spec.history_len()) > 2_000_000 {
|
||||
return Err("pattern_rank_window_budget_exceeded: 完整截面不得截断".into());
|
||||
}
|
||||
}
|
||||
Ok(spec)
|
||||
}
|
||||
fn validate_with_context(mut self, context_fields: &[&str]) -> Result<Self, String> {
|
||||
if (self.template == "expression") != self.expression.is_some() {
|
||||
@@ -82,7 +100,7 @@ impl PatternSpec {
|
||||
if number < bounds[1].as_f64().unwrap() || number > bounds[2].as_f64().unwrap() {
|
||||
return Err(format!("{key}超出允许范围"));
|
||||
}
|
||||
if key.ends_with("window") || key.ends_with("lookback") || key == "anchor_lag" {
|
||||
if key.ends_with("window") || key.ends_with("lookback") || key == "anchor_lag" || key=="opening_minutes" {
|
||||
if number.fract() != 0.0 {
|
||||
return Err(format!("{key}必须是整数"));
|
||||
}
|
||||
@@ -104,6 +122,7 @@ impl PatternSpec {
|
||||
}
|
||||
pub fn history_len(&self) -> usize {
|
||||
match self.template.as_str() {
|
||||
"session_event"=>1,
|
||||
"expression" => self.n("history_window"),
|
||||
"strength" => self.n("slow_window").max(self.n("momentum_window") + 1),
|
||||
"breakout" => self.n("high_window").max(self.n("volume_window")) + 1,
|
||||
@@ -198,13 +217,14 @@ pub fn evaluate(
|
||||
evaluate_with_context(spec, days, series, &BTreeMap::new(), false)
|
||||
}
|
||||
|
||||
fn evaluate_with_context(
|
||||
pub(crate) fn evaluate_with_context(
|
||||
spec: &PatternSpec,
|
||||
days: &[NaiveDate],
|
||||
series: &PatternSeries,
|
||||
context: &BTreeMap<String, Vec<Option<f64>>>,
|
||||
numeric_output: bool,
|
||||
) -> Result<PatternResult, String> {
|
||||
if spec.template=="session_event" {return Err("session_event_requires_completed_minute_endpoint".into());}
|
||||
if days.len() != spec.history_len() || days.windows(2).any(|w| w[0] >= w[1]) {
|
||||
return Err("pattern_calendar_incomplete: 需要完整、唯一且递增的真实交易日窗口".into());
|
||||
}
|
||||
@@ -616,39 +636,38 @@ pub fn evaluate_dataset(
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
) -> Result<PatternResult, String> {
|
||||
let context = crate::pattern_context::build_dataset_context(spec, data, date)?;
|
||||
evaluate_dataset_context(spec, data, date, symbol, &context)
|
||||
}
|
||||
|
||||
pub fn dataset_series(data: &DataSet, days: &[NaiveDate], symbol: &str) -> PatternSeries {
|
||||
let bars = days.iter().filter_map(|&d| data.market(d, symbol).map(|b| PatternBar {
|
||||
date:d, open:Some(b.open), high:Some(b.high), low:Some(b.low), close:Some(b.close),
|
||||
volume:Some(b.volume as f64), prev_close:Some(b.prev_close),
|
||||
amount:data.factor_numeric_value(d,symbol,"amount"),upper_limit:Some(b.upper_limit),
|
||||
no_limit:data.factor_numeric_value(d,symbol,"no_limit").map(|v|v==1.0),
|
||||
adjustment_factor_backward1:data.factor(d,symbol).and_then(|f|f.adjustment_factor_backward1),
|
||||
paused:Some(b.paused), source_path:None,
|
||||
})).collect();
|
||||
PatternSeries{symbol:symbol.into(),name:data.instrument(symbol).map(|i|i.name.clone()),
|
||||
listed_at:data.instrument(symbol).and_then(|i|i.listed_at),bars}
|
||||
}
|
||||
|
||||
pub fn evaluate_dataset_context(
|
||||
spec: &PatternSpec, data: &DataSet, date: NaiveDate, symbol: &str, context: &ResearchContext,
|
||||
) -> Result<PatternResult,String> {
|
||||
let days = data.calendar().trailing_days(date, spec.history_len());
|
||||
let bars = days
|
||||
.iter()
|
||||
.filter_map(|&d| {
|
||||
data.market(d, symbol).map(|b| PatternBar {
|
||||
date: d,
|
||||
open: Some(b.open),
|
||||
high: Some(b.high),
|
||||
low: Some(b.low),
|
||||
close: Some(b.close),
|
||||
volume: Some(b.volume as f64),
|
||||
prev_close: data.factor_numeric_value(d, symbol, "pre_close"),
|
||||
amount: data.factor_numeric_value(d, symbol, "amount"),
|
||||
upper_limit: Some(b.upper_limit),
|
||||
no_limit: data.factor_numeric_value(d,symbol,"no_limit").map(|v|v==1.0),
|
||||
adjustment_factor_backward1: data
|
||||
.factor(d, symbol)
|
||||
.and_then(|f| f.adjustment_factor_backward1),
|
||||
paused: Some(b.paused),
|
||||
source_path: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
evaluate(
|
||||
spec,
|
||||
&days,
|
||||
&PatternSeries {
|
||||
symbol: symbol.into(),
|
||||
name: None,
|
||||
listed_at: data.instrument(symbol).and_then(|i| i.listed_at),
|
||||
bars,
|
||||
},
|
||||
)
|
||||
let mut fields = context.common.clone();
|
||||
fields.extend(context.by_symbol.get(symbol).cloned().unwrap_or_default());
|
||||
let outside = spec.execution_context.as_ref().is_some_and(|c| c.rank_expression.is_some() && !c.rank_universe.iter().any(|s|s==symbol));
|
||||
if outside {
|
||||
for name in ["scope_rank","scope_percentile"] {fields.insert(name.into(),vec![None;days.len()]);}
|
||||
fields.insert("scope_size".into(),vec![Some(spec.execution_context.as_ref().unwrap().rank_universe.len() as f64);days.len()]);
|
||||
}
|
||||
let mut result = evaluate_with_context(spec,&days,&dataset_series(data,&days,symbol),&fields,false)?;
|
||||
if outside && result.score.is_none() { result.exclusion=Some(json!({"reason":"outside_frozen_rank_universe","symbol":symbol,"signal_date":date})); }
|
||||
result.values["execution_context_latest"]=json!(fields.iter().map(|(k,v)|(k,v.last().copied().flatten())).collect::<BTreeMap<_,_>>());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn evaluate_batch(
|
||||
@@ -677,7 +696,7 @@ pub fn evaluate_batch(
|
||||
)
|
||||
}
|
||||
|
||||
/// Research transport only. Strategy PatternSpec validation still rejects these fields.
|
||||
/// Values are supplied only by the verified research transport or dataset context builder.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ResearchContext {
|
||||
@@ -868,6 +887,8 @@ mod tests {
|
||||
template: template.into(),
|
||||
parameters: BTreeMap::new(),
|
||||
expression: None,
|
||||
execution_context: None,
|
||||
session_event: None,
|
||||
}
|
||||
.validate()
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user