补齐next-open执行日风控回归测试

This commit is contained in:
boris
2026-07-03 21:30:06 +08:00
parent 25001fd3e4
commit f45a5fd0a7
+151 -24
View File
@@ -3948,6 +3948,7 @@ mod tests {
};
use crate::events::OrderStatus;
use crate::instrument::Instrument;
use crate::risk_control::FidcRiskControlConfig;
use crate::rules::ChinaEquityRuleHooks;
use crate::scheduler::{ScheduleRule, ScheduleStage};
use crate::strategy::{OrderIntent, Strategy, StrategyContext, StrategyDecision};
@@ -4109,6 +4110,20 @@ mod tests {
}
}
fn st_candidate(date: NaiveDate) -> CandidateEligibility {
CandidateEligibility {
is_st: true,
..candidate(date)
}
}
fn one_yuan_candidate(date: NaiveDate) -> CandidateEligibility {
CandidateEligibility {
is_one_yuan: true,
..candidate(date)
}
}
fn benchmark(date: NaiveDate) -> BenchmarkSnapshot {
BenchmarkSnapshot {
date,
@@ -4131,24 +4146,44 @@ mod tests {
)
}
fn default_instrument() -> Instrument {
Instrument {
symbol: SYMBOL.to_string(),
name: "Test Stock".to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: None,
status: "active".to_string(),
}
}
fn dataset_with(
first_market: DailyMarketSnapshot,
second_market: DailyMarketSnapshot,
first_candidate: CandidateEligibility,
second_candidate: CandidateEligibility,
) -> DataSet {
dataset_with_instrument(
default_instrument(),
first_market,
second_market,
first_candidate,
second_candidate,
)
}
fn dataset_with_instrument(
instrument: Instrument,
first_market: DailyMarketSnapshot,
second_market: DailyMarketSnapshot,
first_candidate: CandidateEligibility,
second_candidate: CandidateEligibility,
) -> DataSet {
let first = first_market.date;
let second = second_market.date;
DataSet::from_components(
vec![Instrument {
symbol: SYMBOL.to_string(),
name: "Test Stock".to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: None,
status: "active".to_string(),
}],
vec![instrument],
vec![first_market, second_market],
vec![factor(first), factor(second)],
vec![first_candidate, second_candidate],
@@ -4194,16 +4229,32 @@ mod tests {
}
fn run_scheduled_next_open_with_dataset(dataset: DataSet) -> super::BacktestResult {
let first = d(2025, 1, 2);
let broker = BrokerSimulator::new_with_execution_price(
run_scheduled_next_open_with_dataset_and_broker(
dataset,
scheduled_next_open_broker(FidcRiskControlConfig::default()),
)
}
fn scheduled_next_open_broker(
risk_config: FidcRiskControlConfig,
) -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_matching_type(MatchingType::NextBarOpen)
.with_risk_config(risk_config)
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_inactive_limit(false);
.with_inactive_limit(false)
}
fn run_scheduled_next_open_with_dataset_and_broker(
dataset: DataSet,
broker: BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
) -> super::BacktestResult {
let first = d(2025, 1, 2);
let config = BacktestConfig {
initial_cash: 100_000.0,
benchmark_code: "000852.SH".to_string(),
@@ -4226,6 +4277,16 @@ mod tests {
.expect("backtest run")
}
fn assert_next_open_canceled_with_reason(result: &super::BacktestResult, reason: &str) {
let execution_date = d(2025, 1, 3);
assert!(result.fills.is_empty());
assert!(result.order_events.iter().any(|event| {
event.date == execution_date
&& matches!(event.status, OrderStatus::Canceled | OrderStatus::Rejected)
&& event.reason.contains(reason)
}));
}
#[test]
fn current_bar_close_uses_decision_day_close_for_fill() {
let result = run_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
@@ -4276,6 +4337,22 @@ mod tests {
assert_eq!(result.fills[0].price, 12.0);
}
#[test]
fn next_bar_open_execution_risk_ignores_decision_day_st_state() {
let first = d(2025, 1, 2);
let second = d(2025, 1, 3);
let result = run_scheduled_next_open_with_dataset(dataset_with(
market(first, 10.0, 11.5),
market(second, 12.0, 99.0),
st_candidate(first),
candidate(second),
));
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].date, second);
assert_eq!(result.fills[0].price, 12.0);
}
#[test]
fn next_bar_open_execution_risk_rejects_execution_day_paused_state() {
let first = d(2025, 1, 2);
@@ -4287,12 +4364,7 @@ mod tests {
candidate_with_state(second, true, true),
));
assert!(result.fills.is_empty());
assert!(result.order_events.iter().any(|event| {
event.date == second
&& event.status == OrderStatus::Canceled
&& event.reason.contains("paused")
}));
assert_next_open_canceled_with_reason(&result, "paused");
}
#[test]
@@ -4306,11 +4378,66 @@ mod tests {
candidate(second),
));
assert!(result.fills.is_empty());
assert!(result.order_events.iter().any(|event| {
event.date == second
&& event.status == OrderStatus::Canceled
&& event.reason.contains("open at or above upper limit")
}));
assert_next_open_canceled_with_reason(&result, "open at or above upper limit");
}
#[test]
fn next_bar_open_execution_risk_rejects_execution_day_st_state() {
let first = d(2025, 1, 2);
let second = d(2025, 1, 3);
let result = run_scheduled_next_open_with_dataset(dataset_with(
market(first, 10.0, 11.5),
market(second, 12.0, 99.0),
candidate(first),
st_candidate(second),
));
assert_next_open_canceled_with_reason(&result, "st");
}
#[test]
fn next_bar_open_execution_risk_rejects_execution_day_one_yuan_state() {
let first = d(2025, 1, 2);
let second = d(2025, 1, 3);
let result = run_scheduled_next_open_with_dataset(dataset_with(
market(first, 10.0, 11.5),
market(second, 12.0, 99.0),
candidate(first),
one_yuan_candidate(second),
));
assert_next_open_canceled_with_reason(&result, "one_yuan");
}
#[test]
fn next_bar_open_execution_risk_rejects_execution_day_delisted_state() {
let first = d(2025, 1, 2);
let second = d(2025, 1, 3);
let mut instrument = default_instrument();
instrument.delisted_at = Some(second);
let result = run_scheduled_next_open_with_dataset(dataset_with_instrument(
instrument,
market(first, 10.0, 11.5),
market(second, 12.0, 99.0),
candidate(first),
candidate(second),
));
assert_next_open_canceled_with_reason(&result, "inactive_or_delisted");
}
#[test]
fn next_bar_open_execution_risk_rejects_blacklisted_buy_on_execution_day() {
let mut risk_config = FidcRiskControlConfig::default();
risk_config
.static_rules
.blacklisted_symbols
.insert(SYMBOL.to_string());
let result = run_scheduled_next_open_with_dataset_and_broker(
dataset(),
scheduled_next_open_broker(risk_config),
);
assert_next_open_canceled_with_reason(&result, "blacklisted");
}
}