fix: reject invalid historical slippage bounds before execution
This commit is contained in:
@@ -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()
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user