fix: reject invalid historical slippage bounds before execution

This commit is contained in:
boris
2026-09-11 11:57:59 +08:00
parent 1fc8a3a9e6
commit e47228beff
2 changed files with 34 additions and 9 deletions
@@ -15008,6 +15008,31 @@ mod tests {
assert_eq!(shared.version_sha256(),shared_version);
}
#[test]
fn projected_historical_slippage_does_not_swallow_missing_calibration() {
let date = d(2025, 1, 7);
let symbol = "000001.SZ";
let data = single_symbol_platform_data(&[date], symbol);
let portfolio = PortfolioState::new(100_000.0);
let subscriptions = BTreeSet::new();
let ctx = StrategyContext {
execution_date: date, decision_date: date, decision_index: 0, data: &data, portfolio: &portfolio,
futures_account: None, open_orders: &[], dynamic_universe: None, subscriptions: &subscriptions,
process_events: &[], active_process_event: None, active_datetime: Some(date.and_hms_opt(15, 0, 0).unwrap()),
order_events: &[], fills: &[],
};
let mut config = PlatformExprStrategyConfig::generic();
config.signal_symbol = symbol.into();
config.slippage_model = SlippageModel::HistoricalVolumeVolatility(crate::DynamicSlippageConfig::default());
let strategy = PlatformExprStrategy::new(config);
let mut projected = portfolio.clone();
let result = strategy.project_order_value(&ctx, &mut projected, date, symbol, 50_000.0, &mut ProjectedExecutionState::default());
let error = result.expect_err("calibration failures must reach the strategy caller");
assert!(error.to_string().contains("historical_slippage_calibration_missing"), "{error}");
assert_eq!(projected.cash(), portfolio.cash());
assert!(projected.positions().is_empty());
}
#[test]
fn portfolio_loss_observes_finalized_nav_after_fees_and_cash_flows() {
use std::sync::Mutex;
@@ -1579,10 +1579,6 @@ fn parse_slippage_model(
return Err(format!("{name} must be finite and non-negative"));
}
}
let value = valid_non_negative(value);
let impact_coefficient = valid_non_negative(impact_coefficient);
let volatility_coefficient = valid_non_negative(volatility_coefficient);
let max_value = valid_non_negative(max_value);
let model = model
.map(normalize_slippage_model_name)
.filter(|item| !item.is_empty())
@@ -1599,11 +1595,15 @@ fn parse_slippage_model(
"price_ratio" => Ok(SlippageModel::PriceRatio(value.unwrap_or(0.0))),
"tick_size" => Ok(SlippageModel::TickSize(value.unwrap_or(0.0))),
"limit_price" => Ok(SlippageModel::LimitPrice),
"historical_volume_volatility" => Ok(SlippageModel::HistoricalVolumeVolatility(DynamicSlippageConfig::new(
impact_coefficient.unwrap_or(0.5),
volatility_coefficient.unwrap_or(0.3),
max_value.or(value).unwrap_or(0.01),
))),
"historical_volume_volatility" => {
let max_ratio = max_value.or(value).unwrap_or(0.01);
if max_ratio >= 1.0 {
return Err("historical slippage maximum must be less than 1".into());
}
Ok(SlippageModel::HistoricalVolumeVolatility(DynamicSlippageConfig::new(
impact_coefficient.unwrap_or(0.5), volatility_coefficient.unwrap_or(0.3), max_ratio,
)))
},
"dynamic" | "dynamic_volume_volatility" => Err(
"retired_slippage_model: dynamic used unfinished daily data; explicitly select historical_volume_volatility or another supported model".into()
),