From 32b6da5aca9ca8f77d3ffebc067666efd388d4f0 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 3 Jul 2026 04:30:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=92=E7=BB=9D=E9=9D=9E=E6=B3=95=E5=9B=9E?= =?UTF-8?q?=E6=B5=8B=E6=88=90=E4=BA=A4=E9=87=8F=E6=AF=94=E4=BE=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_strategy_spec.rs | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index 27c0c93..71574c9 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -668,18 +668,17 @@ fn valid_non_negative(value: Option) -> Option { value.filter(|item| item.is_finite() && *item >= 0.0) } -fn normalize_percent_ratio(value: Option) -> Option { - value - .filter(|item| item.is_finite() && *item > 0.0) - .and_then(|item| { - if item <= 1.0 { - Some(item) - } else if item <= 100.0 { - Some(item / 100.0) - } else { - None - } - }) +fn normalize_percent_ratio(value: f64, field_name: &str) -> Result { + if !value.is_finite() || value <= 0.0 { + return Err(format!("{field_name} must be a positive finite number")); + } + if value <= 1.0 { + Ok(value) + } else if value <= 100.0 { + Ok(value / 100.0) + } else { + Err(format!("{field_name} must be <= 100")) + } } fn parse_policy_date(value: Option<&str>) -> Option { @@ -734,27 +733,29 @@ fn apply_flat_risk_overrides( volume_limit: Option, liquidity_limit: Option, volume_percent: Option, -) { +) -> Result<(), String> { if let Some(enabled) = volume_limit { cfg.risk_config.trading_constraints.volume_limit_enabled = enabled; } if let Some(enabled) = liquidity_limit { cfg.risk_config.trading_constraints.liquidity_limit_enabled = enabled; } - if let Some(value) = normalize_percent_ratio(volume_percent) { + if let Some(raw_value) = volume_percent { + let value = normalize_percent_ratio(raw_value, "volume_percent")?; cfg.risk_config.trading_constraints.volume_percent = value; } cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled || cfg.risk_config.trading_constraints.liquidity_limit_enabled || cfg.matching_type != MatchingType::MinuteLast; + Ok(()) } fn apply_risk_policy_overrides( cfg: &mut PlatformExprStrategyConfig, policy: Option<&StrategyRiskPolicySpec>, -) { +) -> Result<(), String> { let Some(policy) = policy else { - return; + return Ok(()); }; let static_rules = &mut cfg.risk_config.static_rules; if let Some(value) = policy.reject_st_selection { @@ -834,7 +835,7 @@ fn apply_risk_policy_overrides( policy.volume_limit_enabled, policy.liquidity_limit_enabled, policy.volume_percent, - ); + )?; apply_cost_overrides( cfg, policy.commission_rate, @@ -843,6 +844,7 @@ fn apply_risk_policy_overrides( policy.stamp_tax_rate_after_change, policy.stamp_tax_change_date.as_deref(), ); + Ok(()) } fn normalize_model_name(value: &str) -> String { @@ -1026,7 +1028,7 @@ fn infer_expression_windows( stock_short_explicit: bool, stock_mid_explicit: bool, stock_long_explicit: bool, -) { +) -> Result<(), String> { let mut benchmark_days = Vec::new(); for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] { benchmark_days.extend(prefixed_ma_lookbacks(expr, "benchmark_ma")); @@ -1057,6 +1059,7 @@ fn infer_expression_windows( if !stock_long_explicit && let Some(long) = stock_days.last().copied() { cfg.stock_long_ma_days = long; } + Ok(()) } pub fn platform_expr_config_from_spec( @@ -1188,8 +1191,8 @@ pub fn platform_expr_config_from_spec( engine.volume_limit, engine.liquidity_limit, engine.volume_percent, - ); - apply_risk_policy_overrides(&mut cfg, engine.risk_policy.as_ref()); + )?; + apply_risk_policy_overrides(&mut cfg, engine.risk_policy.as_ref())?; apply_execution_behavior_overrides( &mut cfg, engine.matching_type.as_deref(), @@ -1531,7 +1534,7 @@ pub fn platform_expr_config_from_spec( stock_short_explicit, stock_mid_explicit, stock_long_explicit, - ); + )?; if !cfg.signal_symbol.trim().is_empty() { cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None); @@ -1597,8 +1600,8 @@ pub fn platform_expr_config_from_spec( execution.volume_limit, execution.liquidity_limit, execution.volume_percent, - ); - apply_risk_policy_overrides(&mut cfg, execution.risk_policy.as_ref()); + )?; + apply_risk_policy_overrides(&mut cfg, execution.risk_policy.as_ref())?; apply_execution_behavior_overrides( &mut cfg, execution.matching_type.as_deref(), @@ -2258,6 +2261,37 @@ mod tests { assert_eq!(cfg.matching_type, MatchingType::MinuteLast); } + #[test] + fn rejects_invalid_explicit_volume_percent_in_platform_config() { + let engine_value = serde_json::json!({ + "engine_config": { + "volume_percent": 101 + } + }); + let engine_error = platform_expr_config_from_value("", "", &engine_value) + .expect_err("volume_percent above 100 must fail"); + assert!( + engine_error + .to_string() + .contains("volume_percent must be <= 100") + ); + + let policy_value = serde_json::json!({ + "execution": { + "riskPolicy": { + "volumePercent": 0 + } + } + }); + let policy_error = platform_expr_config_from_value("", "", &policy_value) + .expect_err("zero riskPolicy volumePercent must fail"); + assert!( + policy_error + .to_string() + .contains("volume_percent must be a positive finite number") + ); + } + #[test] fn parses_execution_slippage_overrides_into_platform_config() { let spec = serde_json::json!({