修正next-open信号日选股风控语义
This commit is contained in:
@@ -4150,6 +4150,46 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ScheduledEligibleUniverseBuyStrategy {
|
||||||
|
rule: ScheduleRule,
|
||||||
|
expected_decision_date: NaiveDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Strategy for ScheduledEligibleUniverseBuyStrategy {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"scheduled_eligible_universe_buy"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||||
|
vec![self.rule.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_scheduled(
|
||||||
|
&mut self,
|
||||||
|
ctx: &StrategyContext<'_>,
|
||||||
|
rule: &ScheduleRule,
|
||||||
|
) -> Result<StrategyDecision, super::BacktestError> {
|
||||||
|
assert_eq!(rule.name, self.rule.name);
|
||||||
|
assert_eq!(ctx.decision_date, self.expected_decision_date);
|
||||||
|
let Some(symbol) = ctx
|
||||||
|
.eligible_universe_on(ctx.decision_date)
|
||||||
|
.first()
|
||||||
|
.map(|row| row.symbol.clone())
|
||||||
|
else {
|
||||||
|
return Ok(StrategyDecision::default());
|
||||||
|
};
|
||||||
|
Ok(StrategyDecision {
|
||||||
|
order_intents: vec![OrderIntent::Shares {
|
||||||
|
symbol,
|
||||||
|
quantity: 100,
|
||||||
|
reason: "eligible_universe_next_open_buy".to_string(),
|
||||||
|
}],
|
||||||
|
..StrategyDecision::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ScheduledDataDateProbeStrategy {
|
struct ScheduledDataDateProbeStrategy {
|
||||||
rule: ScheduleRule,
|
rule: ScheduleRule,
|
||||||
@@ -4777,6 +4817,28 @@ mod tests {
|
|||||||
.map(|snapshot| snapshot.close),
|
.map(|snapshot| snapshot.close),
|
||||||
Some(11.5)
|
Some(11.5)
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
dataset.eligible_universe_on(first).is_empty(),
|
||||||
|
"raw DataSet helper remains a risk-filtered universe"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manual_ctx
|
||||||
|
.eligible_universe_on(first)
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.symbol)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![SYMBOL.to_string()],
|
||||||
|
"lagged StrategyContext should not apply signal-day execution risk to universe helpers"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manual_ctx
|
||||||
|
.eligible_universe_on_with_risk_config(first, &FidcRiskControlConfig::default())
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.symbol)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![SYMBOL.to_string()],
|
||||||
|
"lagged StrategyContext should defer configured selection risk on the signal day"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
manual_ctx.history_bars(SYMBOL, 2, "1d", "close", true),
|
manual_ctx.history_bars(SYMBOL, 2, "1d", "close", true),
|
||||||
vec![11.5]
|
vec![11.5]
|
||||||
@@ -4816,6 +4878,42 @@ mod tests {
|
|||||||
assert_eq!(result.fills[0].price, 12.0);
|
assert_eq!(result.fills[0].price, 12.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk() {
|
||||||
|
let first = d(2025, 1, 2);
|
||||||
|
let second = d(2025, 1, 3);
|
||||||
|
let dataset = dataset_with(
|
||||||
|
market_with_state(first, 10.0, 11.5, true, 11.5, 9.0),
|
||||||
|
market_with_state(second, 12.0, 99.0, false, 200.0, 1.0),
|
||||||
|
candidate_with_state(first, true, false),
|
||||||
|
candidate(second),
|
||||||
|
);
|
||||||
|
let config = BacktestConfig {
|
||||||
|
initial_cash: 100_000.0,
|
||||||
|
benchmark_code: "000852.SH".to_string(),
|
||||||
|
start_date: Some(first),
|
||||||
|
end_date: Some(second),
|
||||||
|
decision_lag_trading_days: 1,
|
||||||
|
execution_price_field: PriceField::Open,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = BacktestEngine::new(
|
||||||
|
dataset,
|
||||||
|
ScheduledEligibleUniverseBuyStrategy {
|
||||||
|
rule: ScheduleRule::daily("daily_eligible_universe_buy", ScheduleStage::OnDay),
|
||||||
|
expected_decision_date: first,
|
||||||
|
},
|
||||||
|
scheduled_next_open_broker(FidcRiskControlConfig::default()),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
.expect("backtest run");
|
||||||
|
|
||||||
|
assert_eq!(result.fills.len(), 1);
|
||||||
|
assert_eq!(result.fills[0].date, second);
|
||||||
|
assert_eq!(result.fills[0].price, 12.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_bar_open_execution_risk_ignores_decision_day_paused_state() {
|
fn next_bar_open_execution_risk_ignores_decision_day_paused_state() {
|
||||||
let first = d(2025, 1, 2);
|
let first = d(2025, 1, 2);
|
||||||
|
|||||||
@@ -526,14 +526,17 @@ impl StrategyContext<'_> {
|
|||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
) -> Vec<crate::data::EligibleUniverseSnapshot> {
|
) -> Vec<crate::data::EligibleUniverseSnapshot> {
|
||||||
let eligible = self.data.eligible_universe_on(date);
|
let eligible = if self.is_lagged_execution() && date == self.decision_date {
|
||||||
|
self.data.fundamental_universe_on(date)
|
||||||
|
} else {
|
||||||
|
self.data.eligible_universe_on(date).to_vec()
|
||||||
|
};
|
||||||
match self.dynamic_universe {
|
match self.dynamic_universe {
|
||||||
Some(symbols) if !symbols.is_empty() => eligible
|
Some(symbols) if !symbols.is_empty() => eligible
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|row| symbols.contains(&row.symbol))
|
.filter(|row| symbols.contains(&row.symbol))
|
||||||
.cloned()
|
|
||||||
.collect(),
|
.collect(),
|
||||||
_ => eligible.to_vec(),
|
_ => eligible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,9 +545,12 @@ impl StrategyContext<'_> {
|
|||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
risk_config: &crate::risk_control::FidcRiskControlConfig,
|
risk_config: &crate::risk_control::FidcRiskControlConfig,
|
||||||
) -> Vec<crate::data::EligibleUniverseSnapshot> {
|
) -> Vec<crate::data::EligibleUniverseSnapshot> {
|
||||||
let eligible = self
|
let eligible = if self.is_lagged_execution() && date == self.decision_date {
|
||||||
.data
|
self.data.fundamental_universe_on(date)
|
||||||
.eligible_universe_on_with_risk_config(date, risk_config);
|
} else {
|
||||||
|
self.data
|
||||||
|
.eligible_universe_on_with_risk_config(date, risk_config)
|
||||||
|
};
|
||||||
match self.dynamic_universe {
|
match self.dynamic_universe {
|
||||||
Some(symbols) if !symbols.is_empty() => eligible
|
Some(symbols) if !symbols.is_empty() => eligible
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user