完善回测风控配置归一化

This commit is contained in:
boris
2026-07-04 05:56:42 +08:00
parent c911e79d88
commit 995dd96117
2 changed files with 112 additions and 27 deletions
+29 -5
View File
@@ -5915,6 +5915,16 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
) -> Vec<EligibleUniverseSnapshot> {
self.selectable_universe_on_with_options(ctx, date, factor_date, false)
}
fn selectable_universe_on_with_options(
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
defer_limit_state_selection_risk: bool,
) -> Vec<EligibleUniverseSnapshot> {
let mut rows = Vec::new();
for factor in ctx.data.factor_snapshots_on(factor_date) {
@@ -5930,13 +5940,21 @@ impl PlatformExprStrategy {
let Some(market) = ctx.data.market(date, &factor.symbol) else {
continue;
};
if !ctx.is_lagged_execution()
&& self
.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market)
.is_some()
if !ctx.is_lagged_execution() {
if let Some(reason) = self.selection_risk_rejection_reason(
ctx,
date,
&factor.symbol,
candidate,
market,
) {
if !(defer_limit_state_selection_risk
&& matches!(reason, "upper_limit" | "lower_limit"))
{
continue;
}
}
}
if !self.stock_passes_universe_exclude(candidate, market) {
continue;
}
@@ -7024,10 +7042,16 @@ impl PlatformExprStrategy {
band_high: f64,
selection_limit: usize,
) -> Result<(Vec<String>, Vec<String>, usize, Vec<String>), BacktestError> {
let universe = self.selectable_universe_on(ctx, date, universe_factor_date);
let mut diagnostics = Vec::new();
let mut candidates = Vec::new();
let quote_usage = self.stock_filter_quote_usage();
let universe = self.selectable_universe_on_with_options(
ctx,
date,
universe_factor_date,
self.selection_uses_intraday_quote_fields()
|| quote_usage != StockFilterQuoteUsage::DailyOnly,
);
let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit);
for candidate in universe {
let stock = self.selection_stock_state_with_factor_date(
+81 -20
View File
@@ -232,7 +232,13 @@ pub struct StrategyRiskPolicySpec {
pub forbid_same_day_rebuy_after_sell: Option<bool>,
#[serde(default, alias = "blacklist_enabled")]
pub blacklist_enabled: Option<bool>,
#[serde(default, alias = "blacklisted_symbols", alias = "blacklist")]
#[serde(
default,
alias = "blacklisted_symbols",
alias = "blacklistedInstruments",
alias = "blacklisted_instruments",
alias = "blacklist"
)]
pub blacklisted_symbols: Vec<String>,
#[serde(default, alias = "volume_limit_enabled")]
pub volume_limit_enabled: Option<bool>,
@@ -331,6 +337,40 @@ fn remove_policy_alias_values(
values
}
fn parse_policy_bool(canonical: &str, value: Value) -> Result<bool, String> {
match value {
Value::Bool(value) => Ok(value),
Value::Number(number) => match number
.as_i64()
.or_else(|| number.as_u64().map(|v| v as i64))
{
Some(0) => Ok(false),
Some(1) => Ok(true),
_ => Err(format!("riskPolicy.{canonical} must be a boolean")),
},
Value::String(raw) => match raw.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "y" | "on" => Ok(true),
"false" | "0" | "no" | "n" | "off" => Ok(false),
_ => Err(format!("riskPolicy.{canonical} must be a boolean")),
},
_ => Err(format!("riskPolicy.{canonical} must be a boolean")),
}
}
fn push_blacklist_symbols_from_str(
raw: &str,
symbols: &mut Vec<Value>,
seen: &mut HashSet<String>,
) {
for item in raw.split([',', ';', '\n', '\r', '|']) {
let key = item.trim().to_string();
if key.is_empty() || !seen.insert(key.clone()) {
continue;
}
symbols.push(Value::String(key));
}
}
fn normalize_risk_policy_object_aliases(
object: &mut serde_json::Map<String, Value>,
) -> Result<(), String> {
@@ -339,10 +379,7 @@ fn normalize_risk_policy_object_aliases(
if !values.is_empty() {
let mut merged = false;
for value in values {
let Some(value) = value.as_bool() else {
return Err(format!("riskPolicy.{canonical} must be a boolean"));
};
merged |= value;
merged |= parse_policy_bool(canonical, value)?;
}
object.insert((*canonical).to_string(), Value::Bool(merged));
}
@@ -358,7 +395,12 @@ fn normalize_risk_policy_object_aliases(
let blacklist_values = remove_policy_alias_values(
object,
"blacklistedSymbols",
&["blacklisted_symbols", "blacklist"],
&[
"blacklisted_symbols",
"blacklistedInstruments",
"blacklisted_instruments",
"blacklist",
],
);
let mut symbols = Vec::<Value>::new();
let mut seen = HashSet::<String>::new();
@@ -371,18 +413,11 @@ fn normalize_risk_policy_object_aliases(
"riskPolicy.blacklistedSymbols must contain only strings".to_string()
);
};
let key = symbol.trim().to_string();
if key.is_empty() || !seen.insert(key.clone()) {
continue;
}
symbols.push(Value::String(key));
push_blacklist_symbols_from_str(symbol, &mut symbols, &mut seen);
}
}
Value::String(item) => {
let key = item.trim().to_string();
if !key.is_empty() && seen.insert(key.clone()) {
symbols.push(Value::String(key));
}
push_blacklist_symbols_from_str(&item, &mut symbols, &mut seen);
}
_ => {
return Err(
@@ -2242,16 +2277,18 @@ mod tests {
let spec = serde_json::json!({
"engine_config": {
"risk_policy": {
"reject_st_selection": false,
"rejectStarStSelection": true,
"reject_st_buy": false,
"rejectStarStBuy": true,
"reject_paused_selection": false,
"reject_st_selection": "off",
"rejectStarStSelection": "yes",
"reject_st_buy": 0,
"rejectStarStBuy": 1,
"reject_paused_selection": "false",
"reject_kcb_buy": false,
"rejectBjseSelection": false,
"reject_bjse_buy": false,
"blacklisted_symbols": ["000001.SZ"],
"blacklist": ["000002.SZ", "000001.SZ"],
"blacklistedInstruments": "000003.SZ|000004.SZ",
"blacklisted_instruments": ["000005.SZ,000006.SZ"],
"volume_limit_enabled": true,
"volume_percent": 25,
"min_commission": 6.0,
@@ -2288,6 +2325,30 @@ mod tests {
.blacklisted_symbols
.contains("000002.SZ")
);
assert!(
cfg.risk_config
.static_rules
.blacklisted_symbols
.contains("000003.SZ")
);
assert!(
cfg.risk_config
.static_rules
.blacklisted_symbols
.contains("000004.SZ")
);
assert!(
cfg.risk_config
.static_rules
.blacklisted_symbols
.contains("000005.SZ")
);
assert!(
cfg.risk_config
.static_rules
.blacklisted_symbols
.contains("000006.SZ")
);
assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12);
assert_eq!(cfg.risk_config.trading_constraints.minimum_commission, 6.0);
assert_eq!(cfg.commission_rate, Some(0.0003));