修复FIDC风控别名归一化

This commit is contained in:
boris
2026-07-02 07:45:56 +08:00
parent 7db0e8da1d
commit b176d2ff6f
+181 -6
View File
@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use chrono::{NaiveDate, NaiveTime};
use serde::{Deserialize, Serialize};
@@ -244,6 +244,170 @@ pub struct StrategyRiskPolicySpec {
pub stamp_tax_change_date: Option<String>,
}
const RISK_POLICY_BOOL_ALIAS_GROUPS: &[(&str, &[&str])] = &[
(
"rejectStSelection",
&[
"reject_st_selection",
"rejectStarStSelection",
"reject_star_st_selection",
],
),
(
"rejectStBuy",
&["reject_st_buy", "rejectStarStBuy", "reject_star_st_buy"],
),
("rejectPausedSelection", &["reject_paused_selection"]),
("rejectPausedBuy", &["reject_paused_buy"]),
("rejectPausedSell", &["reject_paused_sell"]),
("rejectInactiveSelection", &["reject_inactive_selection"]),
("rejectInactiveBuy", &["reject_inactive_buy"]),
("rejectInactiveSell", &["reject_inactive_sell"]),
(
"rejectNewListingSelection",
&["reject_new_listing_selection"],
),
("rejectNewListingBuy", &["reject_new_listing_buy"]),
("rejectKcbSelection", &["reject_kcb_selection"]),
("rejectKcbBuy", &["reject_kcb_buy"]),
("rejectOneYuanSelection", &["reject_one_yuan_selection"]),
("rejectOneYuanBuy", &["reject_one_yuan_buy"]),
("respectAllowBuySell", &["respect_allow_buy_sell"]),
(
"rejectUpperLimitSelection",
&["reject_upper_limit_selection"],
),
(
"rejectLowerLimitSelection",
&["reject_lower_limit_selection"],
),
("rejectUpperLimitBuy", &["reject_upper_limit_buy"]),
("rejectLowerLimitSell", &["reject_lower_limit_sell"]),
(
"forbidSameDayRebuyAfterSell",
&["forbid_same_day_rebuy_after_sell"],
),
("blacklistEnabled", &["blacklist_enabled"]),
("volumeLimitEnabled", &["volume_limit_enabled"]),
("liquidityLimitEnabled", &["liquidity_limit_enabled"]),
];
const RISK_POLICY_VALUE_ALIAS_GROUPS: &[(&str, &[&str])] = &[
("volumePercent", &["volume_percent"]),
("commissionRate", &["commission_rate"]),
(
"minimumCommission",
&["minimum_commission", "minCommission"],
),
(
"stampTaxRateBeforeChange",
&["stamp_tax_rate_before_change"],
),
("stampTaxRateAfterChange", &["stamp_tax_rate_after_change"]),
("stampTaxChangeDate", &["stamp_tax_change_date"]),
];
fn remove_policy_alias_values(
object: &mut serde_json::Map<String, Value>,
canonical: &str,
aliases: &[&str],
) -> Vec<Value> {
let mut values = Vec::new();
if let Some(value) = object.remove(canonical).filter(|value| !value.is_null()) {
values.push(value);
}
for alias in aliases {
if let Some(value) = object.remove(*alias).filter(|value| !value.is_null()) {
values.push(value);
}
}
values
}
fn normalize_risk_policy_object_aliases(
object: &mut serde_json::Map<String, Value>,
) -> Result<(), String> {
for (canonical, aliases) in RISK_POLICY_BOOL_ALIAS_GROUPS {
let values = remove_policy_alias_values(object, canonical, 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;
}
object.insert((*canonical).to_string(), Value::Bool(merged));
}
}
for (canonical, aliases) in RISK_POLICY_VALUE_ALIAS_GROUPS {
let values = remove_policy_alias_values(object, canonical, aliases);
if let Some(value) = values.into_iter().next() {
object.insert((*canonical).to_string(), value);
}
}
let blacklist_values = remove_policy_alias_values(
object,
"blacklistedSymbols",
&["blacklisted_symbols", "blacklist"],
);
let mut symbols = Vec::<Value>::new();
let mut seen = HashSet::<String>::new();
for value in blacklist_values {
match value {
Value::Array(items) => {
for item in items {
let Some(symbol) = item.as_str() else {
return Err(
"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));
}
}
Value::String(item) => {
let key = item.trim().to_string();
if !key.is_empty() && seen.insert(key.clone()) {
symbols.push(Value::String(key));
}
}
_ => {
return Err(
"riskPolicy.blacklistedSymbols must be a string or string array".to_string(),
);
}
}
}
if !symbols.is_empty() {
object.insert("blacklistedSymbols".to_string(), Value::Array(symbols));
}
Ok(())
}
fn normalize_risk_policy_aliases_in_value(value: &mut Value) -> Result<(), String> {
match value {
Value::Object(object) => {
normalize_risk_policy_object_aliases(object)?;
for child in object.values_mut() {
normalize_risk_policy_aliases_in_value(child)?;
}
}
Value::Array(items) => {
for item in items {
normalize_risk_policy_aliases_in_value(item)?;
}
}
_ => {}
}
Ok(())
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DynamicRangeConfig {
@@ -471,7 +635,9 @@ pub fn platform_expr_config_from_value(
return platform_expr_config_from_spec(strategy_id, signal_symbol, None)
.map_err(platform_config_error);
}
let spec = serde_json::from_value::<StrategyRuntimeSpec>(value.clone())?;
let mut value = value.clone();
normalize_risk_policy_aliases_in_value(&mut value).map_err(platform_config_error)?;
let spec = serde_json::from_value::<StrategyRuntimeSpec>(value)?;
platform_expr_config_from_spec(strategy_id, signal_symbol, Some(&spec))
.map_err(platform_config_error)
}
@@ -2031,11 +2197,14 @@ mod tests {
let spec = serde_json::json!({
"engine_config": {
"risk_policy": {
"reject_star_st_selection": false,
"reject_star_st_buy": false,
"reject_st_selection": false,
"rejectStarStSelection": true,
"reject_st_buy": false,
"rejectStarStBuy": true,
"reject_paused_selection": false,
"reject_kcb_buy": false,
"blacklisted_symbols": ["000001.SZ"],
"blacklist": ["000002.SZ", "000001.SZ"],
"volume_limit_enabled": true,
"volume_percent": 25,
"minimum_commission": 6.0,
@@ -2052,8 +2221,8 @@ mod tests {
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert!(!cfg.risk_config.static_rules.reject_st_selection);
assert!(!cfg.risk_config.static_rules.reject_st_buy);
assert!(cfg.risk_config.static_rules.reject_st_selection);
assert!(cfg.risk_config.static_rules.reject_st_buy);
assert!(!cfg.risk_config.static_rules.reject_paused_selection);
assert!(!cfg.risk_config.static_rules.reject_kcb_buy);
assert!(
@@ -2062,6 +2231,12 @@ mod tests {
.blacklisted_symbols
.contains("000001.SZ")
);
assert!(
cfg.risk_config
.static_rules
.blacklisted_symbols
.contains("000002.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));