拒绝非法回测成交量比例配置
This commit is contained in:
@@ -668,18 +668,17 @@ fn valid_non_negative(value: Option<f64>) -> Option<f64> {
|
|||||||
value.filter(|item| item.is_finite() && *item >= 0.0)
|
value.filter(|item| item.is_finite() && *item >= 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_percent_ratio(value: Option<f64>) -> Option<f64> {
|
fn normalize_percent_ratio(value: f64, field_name: &str) -> Result<f64, String> {
|
||||||
value
|
if !value.is_finite() || value <= 0.0 {
|
||||||
.filter(|item| item.is_finite() && *item > 0.0)
|
return Err(format!("{field_name} must be a positive finite number"));
|
||||||
.and_then(|item| {
|
}
|
||||||
if item <= 1.0 {
|
if value <= 1.0 {
|
||||||
Some(item)
|
Ok(value)
|
||||||
} else if item <= 100.0 {
|
} else if value <= 100.0 {
|
||||||
Some(item / 100.0)
|
Ok(value / 100.0)
|
||||||
} else {
|
} else {
|
||||||
None
|
Err(format!("{field_name} must be <= 100"))
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_policy_date(value: Option<&str>) -> Option<NaiveDate> {
|
fn parse_policy_date(value: Option<&str>) -> Option<NaiveDate> {
|
||||||
@@ -734,27 +733,29 @@ fn apply_flat_risk_overrides(
|
|||||||
volume_limit: Option<bool>,
|
volume_limit: Option<bool>,
|
||||||
liquidity_limit: Option<bool>,
|
liquidity_limit: Option<bool>,
|
||||||
volume_percent: Option<f64>,
|
volume_percent: Option<f64>,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
if let Some(enabled) = volume_limit {
|
if let Some(enabled) = volume_limit {
|
||||||
cfg.risk_config.trading_constraints.volume_limit_enabled = enabled;
|
cfg.risk_config.trading_constraints.volume_limit_enabled = enabled;
|
||||||
}
|
}
|
||||||
if let Some(enabled) = liquidity_limit {
|
if let Some(enabled) = liquidity_limit {
|
||||||
cfg.risk_config.trading_constraints.liquidity_limit_enabled = enabled;
|
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.risk_config.trading_constraints.volume_percent = value;
|
||||||
}
|
}
|
||||||
cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled
|
cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled
|
||||||
|| cfg.risk_config.trading_constraints.liquidity_limit_enabled
|
|| cfg.risk_config.trading_constraints.liquidity_limit_enabled
|
||||||
|| cfg.matching_type != MatchingType::MinuteLast;
|
|| cfg.matching_type != MatchingType::MinuteLast;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_risk_policy_overrides(
|
fn apply_risk_policy_overrides(
|
||||||
cfg: &mut PlatformExprStrategyConfig,
|
cfg: &mut PlatformExprStrategyConfig,
|
||||||
policy: Option<&StrategyRiskPolicySpec>,
|
policy: Option<&StrategyRiskPolicySpec>,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
let Some(policy) = policy else {
|
let Some(policy) = policy else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
let static_rules = &mut cfg.risk_config.static_rules;
|
let static_rules = &mut cfg.risk_config.static_rules;
|
||||||
if let Some(value) = policy.reject_st_selection {
|
if let Some(value) = policy.reject_st_selection {
|
||||||
@@ -834,7 +835,7 @@ fn apply_risk_policy_overrides(
|
|||||||
policy.volume_limit_enabled,
|
policy.volume_limit_enabled,
|
||||||
policy.liquidity_limit_enabled,
|
policy.liquidity_limit_enabled,
|
||||||
policy.volume_percent,
|
policy.volume_percent,
|
||||||
);
|
)?;
|
||||||
apply_cost_overrides(
|
apply_cost_overrides(
|
||||||
cfg,
|
cfg,
|
||||||
policy.commission_rate,
|
policy.commission_rate,
|
||||||
@@ -843,6 +844,7 @@ fn apply_risk_policy_overrides(
|
|||||||
policy.stamp_tax_rate_after_change,
|
policy.stamp_tax_rate_after_change,
|
||||||
policy.stamp_tax_change_date.as_deref(),
|
policy.stamp_tax_change_date.as_deref(),
|
||||||
);
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_model_name(value: &str) -> String {
|
fn normalize_model_name(value: &str) -> String {
|
||||||
@@ -1026,7 +1028,7 @@ fn infer_expression_windows(
|
|||||||
stock_short_explicit: bool,
|
stock_short_explicit: bool,
|
||||||
stock_mid_explicit: bool,
|
stock_mid_explicit: bool,
|
||||||
stock_long_explicit: bool,
|
stock_long_explicit: bool,
|
||||||
) {
|
) -> Result<(), String> {
|
||||||
let mut benchmark_days = Vec::new();
|
let mut benchmark_days = Vec::new();
|
||||||
for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] {
|
for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] {
|
||||||
benchmark_days.extend(prefixed_ma_lookbacks(expr, "benchmark_ma"));
|
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() {
|
if !stock_long_explicit && let Some(long) = stock_days.last().copied() {
|
||||||
cfg.stock_long_ma_days = long;
|
cfg.stock_long_ma_days = long;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn platform_expr_config_from_spec(
|
pub fn platform_expr_config_from_spec(
|
||||||
@@ -1188,8 +1191,8 @@ pub fn platform_expr_config_from_spec(
|
|||||||
engine.volume_limit,
|
engine.volume_limit,
|
||||||
engine.liquidity_limit,
|
engine.liquidity_limit,
|
||||||
engine.volume_percent,
|
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(
|
apply_execution_behavior_overrides(
|
||||||
&mut cfg,
|
&mut cfg,
|
||||||
engine.matching_type.as_deref(),
|
engine.matching_type.as_deref(),
|
||||||
@@ -1531,7 +1534,7 @@ pub fn platform_expr_config_from_spec(
|
|||||||
stock_short_explicit,
|
stock_short_explicit,
|
||||||
stock_mid_explicit,
|
stock_mid_explicit,
|
||||||
stock_long_explicit,
|
stock_long_explicit,
|
||||||
);
|
)?;
|
||||||
|
|
||||||
if !cfg.signal_symbol.trim().is_empty() {
|
if !cfg.signal_symbol.trim().is_empty() {
|
||||||
cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None);
|
cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None);
|
||||||
@@ -1597,8 +1600,8 @@ pub fn platform_expr_config_from_spec(
|
|||||||
execution.volume_limit,
|
execution.volume_limit,
|
||||||
execution.liquidity_limit,
|
execution.liquidity_limit,
|
||||||
execution.volume_percent,
|
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(
|
apply_execution_behavior_overrides(
|
||||||
&mut cfg,
|
&mut cfg,
|
||||||
execution.matching_type.as_deref(),
|
execution.matching_type.as_deref(),
|
||||||
@@ -2258,6 +2261,37 @@ mod tests {
|
|||||||
assert_eq!(cfg.matching_type, MatchingType::MinuteLast);
|
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]
|
#[test]
|
||||||
fn parses_execution_slippage_overrides_into_platform_config() {
|
fn parses_execution_slippage_overrides_into_platform_config() {
|
||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user