补齐next-open执行日风控回归测试
This commit is contained in:
+151
-24
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user