收敛平台策略选股风控判定

This commit is contained in:
boris
2026-07-04 05:19:22 +08:00
parent 723d2c8354
commit f507c63069
+181 -14
View File
@@ -13,9 +13,7 @@ use crate::data::{
use crate::engine::BacktestError;
use crate::events::OrderSide;
use crate::portfolio::PortfolioState;
use crate::risk_control::{
ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit, RiskCheckScope,
};
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
use crate::scheduler::{
ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
};
@@ -6036,22 +6034,13 @@ impl PlatformExprStrategy {
candidate: &crate::data::CandidateEligibility,
market: &DailyMarketSnapshot,
) -> Option<&'static str> {
if let Some(reason) = ChinaAShareRiskControl::baseline_rejection_reason_with_config(
ChinaAShareRiskControl::selection_rejection_reason_with_config(
date,
candidate,
market,
ctx.data.instrument(symbol),
&self.config.risk_config,
RiskCheckScope::Selection,
) {
return Some(reason);
}
if self.config.risk_config.static_rules.respect_allow_buy_sell
&& (!candidate.allow_buy || !candidate.allow_sell)
{
return Some("trade_disabled");
}
None
)
}
fn stock_selection_limit_rejection_reason(
@@ -8635,6 +8624,184 @@ mod tests {
assert_eq!(universe[0].symbol, symbol);
}
#[test]
fn platform_selection_uses_complete_static_risk_policy_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: date,
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: &[],
};
let default_strategy =
PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
assert!(
default_strategy
.selectable_universe_on(&ctx, date, date)
.is_empty()
);
let rejected_rules = default_strategy
.selection_risk_decisions(&ctx, date, date)
.into_iter()
.map(|decision| (decision.symbol, decision.rule_code))
.collect::<BTreeSet<_>>();
assert_eq!(
rejected_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()),
])
);
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.risk_config.static_rules.reject_upper_limit_selection = false;
cfg.risk_config.static_rules.reject_lower_limit_selection = false;
cfg.risk_config.static_rules.reject_new_listing_selection = false;
cfg.risk_config.static_rules.reject_inactive_selection = false;
let relaxed_strategy = PlatformExprStrategy::new(cfg);
let relaxed_universe = relaxed_strategy.selectable_universe_on(&ctx, date, date);
assert_eq!(
relaxed_universe
.iter()
.map(|row| row.symbol.clone())
.collect::<BTreeSet<_>>(),
symbols
.iter()
.map(|symbol| (*symbol).to_string())
.collect::<BTreeSet<_>>()
);
assert!(
relaxed_strategy
.selection_risk_decisions(&ctx, date, date)
.is_empty()
);
}
#[test]
fn platform_selection_and_buy_respect_allow_flags_are_configurable() {
let date = d(2025, 1, 2);