Skip to main content

rustrade_execution/
fill.rs

1use rust_decimal::Decimal;
2use rustrade_instrument::Side;
3use serde::{Deserialize, Serialize};
4
5/// Simulates the execution price for a backtest fill.
6///
7/// Called by the mock exchange engine when deciding at what price a pending
8/// order should be filled against incoming market data.
9///
10/// # Backtest-only
11///
12/// This trait is only relevant for simulated execution via `MockExchange`.
13/// Live execution clients receive real fill prices from the venue — they do
14/// not use `FillModel`.
15///
16/// # Extensibility
17///
18/// Implement this trait to model slippage, market impact, or other execution
19/// dynamics. The built-in models ([`LastPriceFillModel`], [`BidAskFillModel`],
20/// [`MidpointFillModel`]) provide baseline behavior; custom implementations
21/// can add fixed or random slippage, volume-based price impact, or other
22/// realistic execution simulation.
23///
24/// # Arguments
25///
26/// * `side` — order side (Buy or Sell).
27/// * `order_price` — limit price if limit order; `None` for market orders.
28/// * `best_bid` — current best bid in the order book, if available.
29/// * `best_ask` — current best ask in the order book, if available.
30/// * `last_price` — most recent trade price, if available.
31///
32/// Returns `None` if insufficient market data is available to determine a
33/// fill price (e.g. no prices at all on the first tick of a backtest).
34pub trait FillModel {
35    fn fill_price(
36        &self,
37        side: Side,
38        order_price: Option<Decimal>,
39        best_bid: Option<Decimal>,
40        best_ask: Option<Decimal>,
41        last_price: Option<Decimal>,
42    ) -> Option<Decimal>;
43}
44
45/// Fills at the last trade price for market orders, or the order's limit
46/// price for limit orders.
47///
48/// Fallback chain: `order_price` → `last_price` → `best_ask` (Buy) / `best_bid` (Sell).
49///
50/// The simplest fill model — ignores spread entirely. Useful when spread
51/// modeling is handled elsewhere or when deterministic fills are preferred.
52#[derive(
53    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
54)]
55pub struct LastPriceFillModel;
56
57impl FillModel for LastPriceFillModel {
58    fn fill_price(
59        &self,
60        side: Side,
61        order_price: Option<Decimal>,
62        best_bid: Option<Decimal>,
63        best_ask: Option<Decimal>,
64        last_price: Option<Decimal>,
65    ) -> Option<Decimal> {
66        order_price.or(last_price).or(match side {
67            Side::Buy => best_ask,
68            Side::Sell => best_bid,
69        })
70    }
71}
72
73/// Fills market orders at the current best ask (buys) or best bid (sells),
74/// crossing the spread as a market order taker would.
75///
76/// Limit orders fill at the limit price (the price is already favorable
77/// relative to the market when fill is triggered).
78///
79/// Falls back to `last_price` if bid/ask are not available. This model is
80/// more realistic than [`LastPriceFillModel`] for strategies that frequently
81/// cross the spread.
82#[derive(
83    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
84)]
85pub struct BidAskFillModel;
86
87impl FillModel for BidAskFillModel {
88    fn fill_price(
89        &self,
90        side: Side,
91        order_price: Option<Decimal>,
92        best_bid: Option<Decimal>,
93        best_ask: Option<Decimal>,
94        last_price: Option<Decimal>,
95    ) -> Option<Decimal> {
96        if let Some(limit) = order_price {
97            // Limit order: fill at the limit price (caller is responsible for
98            // only calling fill_price when the limit is marketable).
99            return Some(limit);
100        }
101        // Market order: taker crosses the spread.
102        match side {
103            Side::Buy => best_ask.or(last_price),
104            Side::Sell => best_bid.or(last_price),
105        }
106    }
107}
108
109/// Fills at the midpoint of best bid and best ask.
110///
111/// Falls back to `order_price`, then `last_price` when the book is incomplete.
112///
113/// Useful when modelling execution quality between taker (crossing spread)
114/// and maker (resting at the limit), or when bid/ask data is always
115/// available in the backtest feed.
116///
117/// # Note on `order_price` (limit orders)
118///
119/// Unlike [`BidAskFillModel`], this model does **not** honour `order_price`
120/// when both bid and ask are present — it always fills at the midpoint
121/// regardless of the limit price. When the book is incomplete (only one
122/// side present or neither), `order_price` is preferred over `last_price`
123/// to avoid a fill at a worse price than the limit due to a stale last-trade
124/// price (above the limit for buys, below the limit for sells).
125///
126/// The caller is responsible for invoking `fill_price` only when a limit
127/// order is marketable (i.e., the limit has already been crossed). Using
128/// `MidpointFillModel` for strategies that require limit-price guarantees
129/// may result in fills at the midpoint rather than the limit.
130#[derive(
131    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
132)]
133pub struct MidpointFillModel;
134
135impl FillModel for MidpointFillModel {
136    fn fill_price(
137        &self,
138        _side: Side,
139        order_price: Option<Decimal>,
140        best_bid: Option<Decimal>,
141        best_ask: Option<Decimal>,
142        last_price: Option<Decimal>,
143    ) -> Option<Decimal> {
144        match (best_bid, best_ask) {
145            (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
146            _ => order_price.or(last_price),
147        }
148    }
149}
150
151/// Enum-dispatched fill model for use in types that require `Clone`,
152/// `Serialize`, and `Deserialize` (e.g. `MockExchangeConfig`).
153///
154/// Prefer this over `Box<dyn FillModel>` when the field must be part of
155/// a derived `serde` struct. Defaults to [`LastPriceFillModel`].
156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
157pub enum SimFillConfig {
158    LastPrice(LastPriceFillModel),
159    BidAsk(BidAskFillModel),
160    Midpoint(MidpointFillModel),
161}
162
163impl Default for SimFillConfig {
164    fn default() -> Self {
165        Self::LastPrice(LastPriceFillModel)
166    }
167}
168
169impl FillModel for SimFillConfig {
170    fn fill_price(
171        &self,
172        side: Side,
173        order_price: Option<Decimal>,
174        best_bid: Option<Decimal>,
175        best_ask: Option<Decimal>,
176        last_price: Option<Decimal>,
177    ) -> Option<Decimal> {
178        match self {
179            SimFillConfig::LastPrice(m) => {
180                m.fill_price(side, order_price, best_bid, best_ask, last_price)
181            }
182            SimFillConfig::BidAsk(m) => {
183                m.fill_price(side, order_price, best_bid, best_ask, last_price)
184            }
185            SimFillConfig::Midpoint(m) => {
186                m.fill_price(side, order_price, best_bid, best_ask, last_price)
187            }
188        }
189    }
190}
191
192#[cfg(test)]
193#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
194mod tests {
195    use super::*;
196
197    fn d(s: &str) -> Decimal {
198        s.parse().unwrap()
199    }
200
201    fn prices() -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
202        (Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
203    }
204
205    #[test]
206    fn last_price_market_buy_uses_last() {
207        let (bid, ask, last) = prices();
208        assert_eq!(
209            LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
210            Some(d("100.0"))
211        );
212    }
213
214    #[test]
215    fn last_price_limit_uses_order_price() {
216        let (bid, ask, last) = prices();
217        assert_eq!(
218            LastPriceFillModel.fill_price(Side::Buy, Some(d("99.0")), bid, ask, last),
219            Some(d("99.0"))
220        );
221    }
222
223    #[test]
224    fn bid_ask_market_buy_uses_ask() {
225        let (bid, ask, last) = prices();
226        assert_eq!(
227            BidAskFillModel.fill_price(Side::Buy, None, bid, ask, last),
228            Some(d("100.5"))
229        );
230    }
231
232    #[test]
233    fn bid_ask_market_sell_uses_bid() {
234        let (bid, ask, last) = prices();
235        assert_eq!(
236            BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
237            Some(d("99.5"))
238        );
239    }
240
241    #[test]
242    fn midpoint_uses_mid() {
243        let (bid, ask, last) = prices();
244        assert_eq!(
245            MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
246            Some(d("100.0"))
247        );
248    }
249
250    #[test]
251    fn midpoint_falls_back_to_last_when_no_bid_ask() {
252        assert_eq!(
253            MidpointFillModel.fill_price(Side::Buy, None, None, None, Some(d("100.0"))),
254            Some(d("100.0"))
255        );
256    }
257
258    // --- SimFillConfig enum dispatch ---
259
260    #[test]
261    fn fill_model_config_last_price_dispatches() {
262        let (bid, ask, last) = prices();
263        let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
264        assert_eq!(
265            cfg.fill_price(Side::Buy, None, bid, ask, last),
266            LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
267        );
268    }
269
270    #[test]
271    fn fill_model_config_bid_ask_dispatches() {
272        let (bid, ask, last) = prices();
273        let cfg = SimFillConfig::BidAsk(BidAskFillModel);
274        assert_eq!(
275            cfg.fill_price(Side::Sell, None, bid, ask, last),
276            BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
277        );
278    }
279
280    #[test]
281    fn fill_model_config_midpoint_dispatches() {
282        let (bid, ask, last) = prices();
283        let cfg = SimFillConfig::Midpoint(MidpointFillModel);
284        assert_eq!(
285            cfg.fill_price(Side::Buy, None, bid, ask, last),
286            MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
287        );
288    }
289
290    #[test]
291    fn fill_model_config_default_is_last_price() {
292        assert_eq!(
293            SimFillConfig::default(),
294            SimFillConfig::LastPrice(LastPriceFillModel)
295        );
296    }
297
298    // --- Edge cases ---
299
300    #[test]
301    fn last_price_all_none_returns_none() {
302        // No market data at all — e.g. first tick of a backtest before any prices arrive.
303        // The mock exchange falls back to request.state.price when fill_price returns None.
304        assert_eq!(
305            LastPriceFillModel.fill_price(Side::Buy, None, None, None, None),
306            None
307        );
308        assert_eq!(
309            LastPriceFillModel.fill_price(Side::Sell, None, None, None, None),
310            None
311        );
312    }
313
314    #[test]
315    fn last_price_falls_back_to_bid_ask_when_no_last_price() {
316        // When last_price=None but bid/ask are present, the model falls back to
317        // bid/ask (as documented in the fallback chain). This exercises the tertiary
318        // fallback that was previously untested.
319        assert_eq!(
320            LastPriceFillModel.fill_price(Side::Buy, None, Some(d("99.5")), Some(d("100.5")), None),
321            Some(d("100.5")),
322            "Buy with no last_price should fall back to best_ask"
323        );
324        assert_eq!(
325            LastPriceFillModel.fill_price(
326                Side::Sell,
327                None,
328                Some(d("99.5")),
329                Some(d("100.5")),
330                None
331            ),
332            Some(d("99.5")),
333            "Sell with no last_price should fall back to best_bid"
334        );
335    }
336
337    #[test]
338    fn bid_ask_limit_order_wins_over_bid_ask() {
339        // Limit price must take priority over bid/ask even when both are present.
340        let (bid, ask, last) = prices();
341        let limit = Some(d("98.0"));
342        assert_eq!(
343            BidAskFillModel.fill_price(Side::Buy, limit, bid, ask, last),
344            limit,
345            "limit price should beat best_ask for buy"
346        );
347        assert_eq!(
348            BidAskFillModel.fill_price(Side::Sell, limit, bid, ask, last),
349            limit,
350            "limit price should beat best_bid for sell"
351        );
352    }
353
354    #[test]
355    fn midpoint_with_only_bid_falls_back_to_last() {
356        // Partial book: only bid present, no ask. Should fall back to last_price.
357        assert_eq!(
358            MidpointFillModel.fill_price(Side::Buy, None, Some(d("99.5")), None, Some(d("100.0"))),
359            Some(d("100.0"))
360        );
361    }
362
363    #[test]
364    fn midpoint_with_only_ask_falls_back_to_last() {
365        // Partial book: only ask present, no bid. Should fall back to last_price
366        // (order_price is None, so order_price.or(last_price) = last_price).
367        assert_eq!(
368            MidpointFillModel.fill_price(
369                Side::Sell,
370                None,
371                None,
372                Some(d("100.5")),
373                Some(d("100.0"))
374            ),
375            Some(d("100.0"))
376        );
377    }
378
379    #[test]
380    fn midpoint_partial_book_prefers_order_price_over_last() {
381        // With a partial book (one side missing), limit price takes priority over
382        // a potentially stale last_price. Previously last_price would win, which
383        // could fill a limit buy above its own limit.
384        assert_eq!(
385            MidpointFillModel.fill_price(
386                Side::Buy,
387                Some(d("100.0")),
388                Some(d("99.5")),
389                None,
390                Some(d("110.0"))
391            ),
392            Some(d("100.0")),
393            "partial book: limit price should beat stale last_price"
394        );
395    }
396}