完善回测风控配置归一化
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user