补充next-open选股风控延后回归测试

This commit is contained in:
boris
2026-07-05 17:44:34 +08:00
parent 289448d196
commit 7059d3a8d0
+186 -1
View File
@@ -8686,7 +8686,8 @@ mod tests {
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig, PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
PlatformUniverseActionKind, StockFilterQuoteUsage, precomputed_stock_rolling_mean, PlatformUniverseActionKind, SelectionRiskDeferral, StockFilterQuoteUsage,
precomputed_stock_rolling_mean,
}; };
use crate::{ use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
@@ -9376,6 +9377,190 @@ mod tests {
); );
} }
#[test]
fn platform_next_open_defers_complete_static_selection_risk_before_universe_output() {
let date = d(2025, 1, 2);
let symbols = [
"000001.SZ",
"000002.SZ",
"000003.SZ",
"000004.SZ",
"000005.SZ",
];
let instruments = symbols
.iter()
.map(|symbol| Instrument {
symbol: (*symbol).to_string(),
name: (*symbol).to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: Some(d(2020, 1, 1)),
delisted_at: if *symbol == "000004.SZ" {
Some(date)
} else {
None
},
status: if *symbol == "000004.SZ" {
"delisted".to_string()
} else {
"active".to_string()
},
})
.collect::<Vec<_>>();
let market_rows = symbols
.iter()
.map(|symbol| {
let (last_price, upper_limit, lower_limit) = match *symbol {
"000001.SZ" => (11.0, 11.0, 9.0),
"000002.SZ" => (9.0, 11.0, 9.0),
_ => (10.0, 11.0, 9.0),
};
DailyMarketSnapshot {
date,
symbol: (*symbol).to_string(),
timestamp: None,
day_open: last_price,
open: last_price,
high: last_price,
low: last_price,
close: last_price,
last_price,
bid1: last_price,
ask1: last_price,
prev_close: 10.0,
volume: 1_000_000,
minute_volume: 10_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit,
lower_limit,
price_tick: 0.01,
}
})
.collect::<Vec<_>>();
let factor_rows = symbols
.iter()
.enumerate()
.map(|(index, symbol)| DailyFactorSnapshot {
date,
symbol: (*symbol).to_string(),
market_cap_bn: 10.0 + index as f64,
free_float_cap_bn: 9.0 + index as f64,
pe_ttm: 12.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(),
})
.collect::<Vec<_>>();
let candidate_rows = symbols
.iter()
.map(|symbol| CandidateEligibility {
date,
symbol: (*symbol).to_string(),
is_st: false,
is_star_st: false,
is_new_listing: *symbol == "000003.SZ",
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: if *symbol == "000005.SZ" {
Some("missing_risk_state:active_status".to_string())
} else {
None
},
})
.collect::<Vec<_>>();
let data = DataSet::from_components(
instruments,
market_rows,
factor_rows,
candidate_rows,
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1001.0,
prev_close: 999.0,
volume: 1_000_000,
}],
)
.expect("dataset");
let portfolio = PortfolioState::new(1_000_000.0);
let subscriptions = BTreeSet::new();
let ctx = StrategyContext {
execution_date: d(2025, 1, 3),
decision_date: date,
decision_index: 0,
data: &data,
portfolio: &portfolio,
futures_account: None,
open_orders: &[],
dynamic_universe: None,
subscriptions: &subscriptions,
process_events: &[],
active_process_event: None,
active_datetime: None,
order_events: &[],
fills: &[],
};
assert!(ctx.is_lagged_execution());
let mut strict_cfg = PlatformExprStrategyConfig::microcap_rotation();
strict_cfg
.risk_config
.static_rules
.reject_upper_limit_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_lower_limit_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_new_listing_selection = true;
strict_cfg
.risk_config
.static_rules
.reject_inactive_selection = true;
let strict_strategy = PlatformExprStrategy::new(strict_cfg);
let deferred_universe = strict_strategy.selectable_universe_on_with_options(
&ctx,
date,
date,
SelectionRiskDeferral::All,
);
assert_eq!(
deferred_universe
.iter()
.map(|row| row.symbol.clone())
.collect::<BTreeSet<_>>(),
symbols
.iter()
.map(|symbol| (*symbol).to_string())
.collect::<BTreeSet<_>>()
);
let deferred_rules = strict_strategy
.selection_risk_decisions_with_options(&ctx, date, date, SelectionRiskDeferral::All)
.into_iter()
.map(|decision| (decision.symbol, decision.rule_code))
.collect::<BTreeSet<_>>();
assert_eq!(
deferred_rules,
BTreeSet::from([
("000001.SZ".to_string(), "upper_limit".to_string()),
("000002.SZ".to_string(), "lower_limit".to_string()),
("000003.SZ".to_string(), "new_listing".to_string()),
("000004.SZ".to_string(), "inactive_or_delisted".to_string()),
("000005.SZ".to_string(), "missing_risk_state".to_string()),
])
);
}
#[test] #[test]
fn platform_selection_and_buy_respect_allow_flags_are_configurable() { fn platform_selection_and_buy_respect_allow_flags_are_configurable() {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);