补齐next-open执行日风控回归测试
This commit is contained in:
+151
-24
@@ -3948,6 +3948,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::events::OrderStatus;
|
use crate::events::OrderStatus;
|
||||||
use crate::instrument::Instrument;
|
use crate::instrument::Instrument;
|
||||||
|
use crate::risk_control::FidcRiskControlConfig;
|
||||||
use crate::rules::ChinaEquityRuleHooks;
|
use crate::rules::ChinaEquityRuleHooks;
|
||||||
use crate::scheduler::{ScheduleRule, ScheduleStage};
|
use crate::scheduler::{ScheduleRule, ScheduleStage};
|
||||||
use crate::strategy::{OrderIntent, Strategy, StrategyContext, StrategyDecision};
|
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 {
|
fn benchmark(date: NaiveDate) -> BenchmarkSnapshot {
|
||||||
BenchmarkSnapshot {
|
BenchmarkSnapshot {
|
||||||
date,
|
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(
|
fn dataset_with(
|
||||||
first_market: DailyMarketSnapshot,
|
first_market: DailyMarketSnapshot,
|
||||||
second_market: DailyMarketSnapshot,
|
second_market: DailyMarketSnapshot,
|
||||||
first_candidate: CandidateEligibility,
|
first_candidate: CandidateEligibility,
|
||||||
second_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 {
|
) -> DataSet {
|
||||||
let first = first_market.date;
|
let first = first_market.date;
|
||||||
let second = second_market.date;
|
let second = second_market.date;
|
||||||
DataSet::from_components(
|
DataSet::from_components(
|
||||||
vec![Instrument {
|
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![first_market, second_market],
|
vec![first_market, second_market],
|
||||||
vec![factor(first), factor(second)],
|
vec![factor(first), factor(second)],
|
||||||
vec![first_candidate, second_candidate],
|
vec![first_candidate, second_candidate],
|
||||||
@@ -4194,16 +4229,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_scheduled_next_open_with_dataset(dataset: DataSet) -> super::BacktestResult {
|
fn run_scheduled_next_open_with_dataset(dataset: DataSet) -> super::BacktestResult {
|
||||||
let first = d(2025, 1, 2);
|
run_scheduled_next_open_with_dataset_and_broker(
|
||||||
let broker = BrokerSimulator::new_with_execution_price(
|
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(),
|
ChinaAShareCostModel::default(),
|
||||||
ChinaEquityRuleHooks,
|
ChinaEquityRuleHooks,
|
||||||
PriceField::Open,
|
PriceField::Open,
|
||||||
)
|
)
|
||||||
.with_matching_type(MatchingType::NextBarOpen)
|
.with_matching_type(MatchingType::NextBarOpen)
|
||||||
|
.with_risk_config(risk_config)
|
||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_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 {
|
let config = BacktestConfig {
|
||||||
initial_cash: 100_000.0,
|
initial_cash: 100_000.0,
|
||||||
benchmark_code: "000852.SH".to_string(),
|
benchmark_code: "000852.SH".to_string(),
|
||||||
@@ -4226,6 +4277,16 @@ mod tests {
|
|||||||
.expect("backtest run")
|
.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]
|
#[test]
|
||||||
fn current_bar_close_uses_decision_day_close_for_fill() {
|
fn current_bar_close_uses_decision_day_close_for_fill() {
|
||||||
let result = run_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
let result = run_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
||||||
@@ -4276,6 +4337,22 @@ mod tests {
|
|||||||
assert_eq!(result.fills[0].price, 12.0);
|
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]
|
#[test]
|
||||||
fn next_bar_open_execution_risk_rejects_execution_day_paused_state() {
|
fn next_bar_open_execution_risk_rejects_execution_day_paused_state() {
|
||||||
let first = d(2025, 1, 2);
|
let first = d(2025, 1, 2);
|
||||||
@@ -4287,12 +4364,7 @@ mod tests {
|
|||||||
candidate_with_state(second, true, true),
|
candidate_with_state(second, true, true),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(result.fills.is_empty());
|
assert_next_open_canceled_with_reason(&result, "paused");
|
||||||
assert!(result.order_events.iter().any(|event| {
|
|
||||||
event.date == second
|
|
||||||
&& event.status == OrderStatus::Canceled
|
|
||||||
&& event.reason.contains("paused")
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4306,11 +4378,66 @@ mod tests {
|
|||||||
candidate(second),
|
candidate(second),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(result.fills.is_empty());
|
assert_next_open_canceled_with_reason(&result, "open at or above upper limit");
|
||||||
assert!(result.order_events.iter().any(|event| {
|
}
|
||||||
event.date == second
|
|
||||||
&& event.status == OrderStatus::Canceled
|
#[test]
|
||||||
&& event.reason.contains("open at or above upper limit")
|
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