修复FIDC风控别名归一化
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
|
|
||||||
use chrono::{NaiveDate, NaiveTime};
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -244,6 +244,170 @@ pub struct StrategyRiskPolicySpec {
|
|||||||
pub stamp_tax_change_date: Option<String>,
|
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)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DynamicRangeConfig {
|
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)
|
return platform_expr_config_from_spec(strategy_id, signal_symbol, None)
|
||||||
.map_err(platform_config_error);
|
.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))
|
platform_expr_config_from_spec(strategy_id, signal_symbol, Some(&spec))
|
||||||
.map_err(platform_config_error)
|
.map_err(platform_config_error)
|
||||||
}
|
}
|
||||||
@@ -2031,11 +2197,14 @@ mod tests {
|
|||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
"engine_config": {
|
"engine_config": {
|
||||||
"risk_policy": {
|
"risk_policy": {
|
||||||
"reject_star_st_selection": false,
|
"reject_st_selection": false,
|
||||||
"reject_star_st_buy": false,
|
"rejectStarStSelection": true,
|
||||||
|
"reject_st_buy": false,
|
||||||
|
"rejectStarStBuy": true,
|
||||||
"reject_paused_selection": false,
|
"reject_paused_selection": false,
|
||||||
"reject_kcb_buy": false,
|
"reject_kcb_buy": false,
|
||||||
"blacklisted_symbols": ["000001.SZ"],
|
"blacklisted_symbols": ["000001.SZ"],
|
||||||
|
"blacklist": ["000002.SZ", "000001.SZ"],
|
||||||
"volume_limit_enabled": true,
|
"volume_limit_enabled": true,
|
||||||
"volume_percent": 25,
|
"volume_percent": 25,
|
||||||
"minimum_commission": 6.0,
|
"minimum_commission": 6.0,
|
||||||
@@ -2052,8 +2221,8 @@ mod tests {
|
|||||||
|
|
||||||
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
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_selection);
|
||||||
assert!(!cfg.risk_config.static_rules.reject_st_buy);
|
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_paused_selection);
|
||||||
assert!(!cfg.risk_config.static_rules.reject_kcb_buy);
|
assert!(!cfg.risk_config.static_rules.reject_kcb_buy);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -2062,6 +2231,12 @@ mod tests {
|
|||||||
.blacklisted_symbols
|
.blacklisted_symbols
|
||||||
.contains("000001.SZ")
|
.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!((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