From b176d2ff6f12321e71e182171506c7db0194f88d Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 2 Jul 2026 07:45:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DFIDC=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E5=88=AB=E5=90=8D=E5=BD=92=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_strategy_spec.rs | 187 +++++++++++++++++- 1 file changed, 181 insertions(+), 6 deletions(-) diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index cfea5dd..e7d88d5 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -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, } +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, + canonical: &str, + aliases: &[&str], +) -> Vec { + 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, +) -> 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::::new(); + let mut seen = HashSet::::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::(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::(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));