完善回测风控配置归一化
This commit is contained in:
@@ -5915,6 +5915,16 @@ impl PlatformExprStrategy {
|
|||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
factor_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> {
|
) -> Vec<EligibleUniverseSnapshot> {
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for factor in ctx.data.factor_snapshots_on(factor_date) {
|
for factor in ctx.data.factor_snapshots_on(factor_date) {
|
||||||
@@ -5930,12 +5940,20 @@ impl PlatformExprStrategy {
|
|||||||
let Some(market) = ctx.data.market(date, &factor.symbol) else {
|
let Some(market) = ctx.data.market(date, &factor.symbol) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !ctx.is_lagged_execution()
|
if !ctx.is_lagged_execution() {
|
||||||
&& self
|
if let Some(reason) = self.selection_risk_rejection_reason(
|
||||||
.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market)
|
ctx,
|
||||||
.is_some()
|
date,
|
||||||
{
|
&factor.symbol,
|
||||||
continue;
|
candidate,
|
||||||
|
market,
|
||||||
|
) {
|
||||||
|
if !(defer_limit_state_selection_risk
|
||||||
|
&& matches!(reason, "upper_limit" | "lower_limit"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !self.stock_passes_universe_exclude(candidate, market) {
|
if !self.stock_passes_universe_exclude(candidate, market) {
|
||||||
continue;
|
continue;
|
||||||
@@ -7024,10 +7042,16 @@ impl PlatformExprStrategy {
|
|||||||
band_high: f64,
|
band_high: f64,
|
||||||
selection_limit: usize,
|
selection_limit: usize,
|
||||||
) -> Result<(Vec<String>, Vec<String>, usize, Vec<String>), BacktestError> {
|
) -> 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 diagnostics = Vec::new();
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
let quote_usage = self.stock_filter_quote_usage();
|
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);
|
let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit);
|
||||||
for candidate in universe {
|
for candidate in universe {
|
||||||
let stock = self.selection_stock_state_with_factor_date(
|
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>,
|
pub forbid_same_day_rebuy_after_sell: Option<bool>,
|
||||||
#[serde(default, alias = "blacklist_enabled")]
|
#[serde(default, alias = "blacklist_enabled")]
|
||||||
pub blacklist_enabled: Option<bool>,
|
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>,
|
pub blacklisted_symbols: Vec<String>,
|
||||||
#[serde(default, alias = "volume_limit_enabled")]
|
#[serde(default, alias = "volume_limit_enabled")]
|
||||||
pub volume_limit_enabled: Option<bool>,
|
pub volume_limit_enabled: Option<bool>,
|
||||||
@@ -331,6 +337,40 @@ fn remove_policy_alias_values(
|
|||||||
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(
|
fn normalize_risk_policy_object_aliases(
|
||||||
object: &mut serde_json::Map<String, Value>,
|
object: &mut serde_json::Map<String, Value>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -339,10 +379,7 @@ fn normalize_risk_policy_object_aliases(
|
|||||||
if !values.is_empty() {
|
if !values.is_empty() {
|
||||||
let mut merged = false;
|
let mut merged = false;
|
||||||
for value in values {
|
for value in values {
|
||||||
let Some(value) = value.as_bool() else {
|
merged |= parse_policy_bool(canonical, value)?;
|
||||||
return Err(format!("riskPolicy.{canonical} must be a boolean"));
|
|
||||||
};
|
|
||||||
merged |= value;
|
|
||||||
}
|
}
|
||||||
object.insert((*canonical).to_string(), Value::Bool(merged));
|
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(
|
let blacklist_values = remove_policy_alias_values(
|
||||||
object,
|
object,
|
||||||
"blacklistedSymbols",
|
"blacklistedSymbols",
|
||||||
&["blacklisted_symbols", "blacklist"],
|
&[
|
||||||
|
"blacklisted_symbols",
|
||||||
|
"blacklistedInstruments",
|
||||||
|
"blacklisted_instruments",
|
||||||
|
"blacklist",
|
||||||
|
],
|
||||||
);
|
);
|
||||||
let mut symbols = Vec::<Value>::new();
|
let mut symbols = Vec::<Value>::new();
|
||||||
let mut seen = HashSet::<String>::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()
|
"riskPolicy.blacklistedSymbols must contain only strings".to_string()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
let key = symbol.trim().to_string();
|
push_blacklist_symbols_from_str(symbol, &mut symbols, &mut seen);
|
||||||
if key.is_empty() || !seen.insert(key.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
symbols.push(Value::String(key));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::String(item) => {
|
Value::String(item) => {
|
||||||
let key = item.trim().to_string();
|
push_blacklist_symbols_from_str(&item, &mut symbols, &mut seen);
|
||||||
if !key.is_empty() && seen.insert(key.clone()) {
|
|
||||||
symbols.push(Value::String(key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(
|
return Err(
|
||||||
@@ -2242,16 +2277,18 @@ mod tests {
|
|||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
"engine_config": {
|
"engine_config": {
|
||||||
"risk_policy": {
|
"risk_policy": {
|
||||||
"reject_st_selection": false,
|
"reject_st_selection": "off",
|
||||||
"rejectStarStSelection": true,
|
"rejectStarStSelection": "yes",
|
||||||
"reject_st_buy": false,
|
"reject_st_buy": 0,
|
||||||
"rejectStarStBuy": true,
|
"rejectStarStBuy": 1,
|
||||||
"reject_paused_selection": false,
|
"reject_paused_selection": "false",
|
||||||
"reject_kcb_buy": false,
|
"reject_kcb_buy": false,
|
||||||
"rejectBjseSelection": false,
|
"rejectBjseSelection": false,
|
||||||
"reject_bjse_buy": false,
|
"reject_bjse_buy": false,
|
||||||
"blacklisted_symbols": ["000001.SZ"],
|
"blacklisted_symbols": ["000001.SZ"],
|
||||||
"blacklist": ["000002.SZ", "000001.SZ"],
|
"blacklist": ["000002.SZ", "000001.SZ"],
|
||||||
|
"blacklistedInstruments": "000003.SZ|000004.SZ",
|
||||||
|
"blacklisted_instruments": ["000005.SZ,000006.SZ"],
|
||||||
"volume_limit_enabled": true,
|
"volume_limit_enabled": true,
|
||||||
"volume_percent": 25,
|
"volume_percent": 25,
|
||||||
"min_commission": 6.0,
|
"min_commission": 6.0,
|
||||||
@@ -2288,6 +2325,30 @@ mod tests {
|
|||||||
.blacklisted_symbols
|
.blacklisted_symbols
|
||||||
.contains("000002.SZ")
|
.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!((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.risk_config.trading_constraints.minimum_commission, 6.0);
|
||||||
assert_eq!(cfg.commission_rate, Some(0.0003));
|
assert_eq!(cfg.commission_rate, Some(0.0003));
|
||||||
|
|||||||
Reference in New Issue
Block a user