完善平台策略回测撮合和滑点

This commit is contained in:
boris
2026-05-28 08:59:14 +08:00
parent 3499d4aa74
commit 200d5d1f41
8 changed files with 786 additions and 92 deletions
+116 -28
View File
@@ -80,12 +80,68 @@ pub enum MatchingType {
Twap,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DynamicSlippageConfig {
pub impact_coefficient: f64,
pub volatility_coefficient: f64,
pub max_ratio: f64,
}
impl DynamicSlippageConfig {
pub fn new(impact_coefficient: f64, volatility_coefficient: f64, max_ratio: f64) -> Self {
Self {
impact_coefficient: impact_coefficient.max(0.0),
volatility_coefficient: volatility_coefficient.max(0.0),
max_ratio: max_ratio.max(0.0),
}
}
fn ratio(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
raw_price: f64,
order_value: Option<f64>,
) -> f64 {
let daily_amount = (snapshot.volume as f64 * raw_price).max(0.0);
let impact_ratio = match order_value {
Some(value) if value.is_finite() && value > 0.0 && daily_amount > 0.0 => {
value / daily_amount
}
_ => 0.0,
};
let volatility_base = if snapshot.prev_close.is_finite() && snapshot.prev_close > 0.0 {
snapshot.prev_close
} else {
raw_price
};
let volatility = if snapshot.high.is_finite()
&& snapshot.low.is_finite()
&& volatility_base.is_finite()
&& volatility_base > 0.0
{
((snapshot.high - snapshot.low).abs() / volatility_base).max(0.0)
} else {
0.0
};
let ratio =
impact_ratio * self.impact_coefficient + volatility * self.volatility_coefficient;
ratio.clamp(0.0, self.max_ratio)
}
}
impl Default for DynamicSlippageConfig {
fn default() -> Self {
Self::new(0.5, 0.3, 0.01)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SlippageModel {
None,
PriceRatio(f64),
TickSize(f64),
LimitPrice,
Dynamic(DynamicSlippageConfig),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -306,6 +362,7 @@ where
&self,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
quantity: Option<u32>,
) -> f64 {
let raw_price = if self.execution_price_field == PriceField::Last
&& self.intraday_execution_start_time.is_some()
@@ -319,7 +376,7 @@ where
}
};
self.apply_slippage(snapshot, side, raw_price)
self.apply_slippage(snapshot, side, raw_price, quantity)
}
fn is_open_auction_matching(&self) -> bool {
@@ -331,6 +388,7 @@ where
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
raw_price: f64,
quantity: Option<u32>,
) -> f64 {
if !raw_price.is_finite() || raw_price <= 0.0 {
return raw_price;
@@ -340,6 +398,7 @@ where
return self.clamp_execution_price(snapshot, side, raw_price);
}
let order_value = quantity.and_then(|qty| (qty > 0).then_some(raw_price * qty as f64));
let adjusted = match self.slippage_model {
SlippageModel::None => raw_price,
SlippageModel::PriceRatio(ratio) => {
@@ -358,6 +417,13 @@ where
}
}
SlippageModel::LimitPrice => raw_price,
SlippageModel::Dynamic(config) => {
let ratio = config.ratio(snapshot, raw_price, order_value);
match side {
OrderSide::Buy => raw_price * (1.0 + ratio),
OrderSide::Sell => raw_price * (1.0 - ratio),
}
}
};
self.clamp_execution_price(snapshot, side, adjusted)
@@ -394,8 +460,9 @@ where
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
raw_price: f64,
quantity: Option<u32>,
) -> f64 {
self.apply_slippage(snapshot, side, raw_price)
self.apply_slippage(snapshot, side, raw_price, quantity)
}
fn matching_type_for_algo_request(
@@ -411,7 +478,7 @@ where
fn select_quote_reference_price(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
_snapshot: &crate::data::DailyMarketSnapshot,
quote: &IntradayExecutionQuote,
side: OrderSide,
matching_type: MatchingType,
@@ -462,9 +529,8 @@ where
OrderSide::Sell => quote.sell_price(),
},
}?;
let execution_price = self.quote_execution_price(snapshot, side, raw_price);
if execution_price.is_finite() && execution_price > 0.0 {
Some(execution_price)
if raw_price.is_finite() && raw_price > 0.0 {
Some(raw_price)
} else {
None
}
@@ -2345,7 +2411,8 @@ where
merge_partial_fill_reason(partial_fill_reason, fill.unfilled_reason);
(fill.quantity, fill.legs)
} else {
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Sell);
let mut execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Sell, Some(fillable_qty));
if let Some(reason) =
self.execution_limit_rejection_reason(snapshot, OrderSide::Sell, execution_price)
{
@@ -2363,7 +2430,7 @@ where
);
(0, Vec::new())
} else {
let execution_price =
execution_price =
self.execution_price_with_limit_slippage(execution_price, limit_price);
(
fillable_qty,
@@ -3714,7 +3781,8 @@ where
merge_partial_fill_reason(partial_fill_reason, fill.unfilled_reason);
(fill.quantity, fill.legs)
} else {
let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
let mut execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(constrained_qty));
if let Some(reason) =
self.execution_limit_rejection_reason(snapshot, OrderSide::Buy, execution_price)
{
@@ -3732,7 +3800,7 @@ where
);
(0, Vec::new())
} else {
let execution_price =
execution_price =
self.execution_price_with_limit_slippage(execution_price, limit_price);
let filled_qty = self.affordable_buy_quantity(
date,
@@ -3743,6 +3811,12 @@ where
self.minimum_order_quantity(data, symbol),
self.order_step_size(data, symbol),
);
if filled_qty > 0 {
execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(filled_qty));
execution_price =
self.execution_price_with_limit_slippage(execution_price, limit_price);
}
if filled_qty < constrained_qty {
partial_fill_reason = merge_partial_fill_reason(
partial_fill_reason,
@@ -4537,28 +4611,11 @@ where
// Approximate platform-native market-order fills with the evolving L1 book after
// the decision time instead of trade VWAP. This keeps quantities/prices
// closer to the observed 10:18 execution logs.
let Some(quote_price) =
let Some(raw_quote_price) =
self.select_quote_reference_price(snapshot, quote, side, matching_type)
else {
continue;
};
if let Some(reason) = self.execution_limit_rejection_reason(snapshot, side, quote_price)
{
execution_block_reason.get_or_insert(reason);
execution_block_timestamp = Some(quote.timestamp);
continue;
}
saw_non_blocked_execution_price = true;
if !self.price_satisfies_limit(
side,
quote_price,
limit_price,
snapshot.effective_price_tick(),
) {
continue;
}
let quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
let remaining_qty = requested_qty.saturating_sub(filled_qty);
if remaining_qty == 0 {
break;
@@ -4594,8 +4651,35 @@ where
continue;
}
let mut quote_price =
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
if let Some(reason) = self.execution_limit_rejection_reason(snapshot, side, quote_price)
{
execution_block_reason.get_or_insert(reason);
execution_block_timestamp = Some(quote.timestamp);
continue;
}
saw_non_blocked_execution_price = true;
if !self.price_satisfies_limit(
side,
quote_price,
limit_price,
snapshot.effective_price_tick(),
) {
continue;
}
if let Some(cash) = cash_limit {
while take_qty > 0 {
quote_price =
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
if !quote_price.is_finite() || quote_price <= 0.0 {
budget_block_reason = Some("invalid execution price");
take_qty = 0;
break;
}
quote_price =
self.execution_price_with_limit_slippage(quote_price, limit_price);
let candidate_gross = gross_amount + quote_price * take_qty as f64;
if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
budget_block_reason = Some("value budget limit");
@@ -4621,6 +4705,10 @@ where
}
}
quote_price =
self.quote_execution_price(snapshot, side, raw_quote_price, Some(take_qty));
quote_price = self.execution_price_with_limit_slippage(quote_price, limit_price);
gross_amount += quote_price * take_qty as f64;
filled_qty += take_qty;
last_timestamp = Some(quote.timestamp);