Skip to main content

rustrade_execution/
fill.rs

1use crate::market::{MarketBar, MarketSnapshot};
2use rust_decimal::Decimal;
3use rustrade_instrument::Side;
4use serde::{Deserialize, Serialize};
5use std::{fmt::Debug, time::Duration};
6
7/// Everything a [`FillModel`] may price from.
8///
9/// # Fields are added, never removed
10///
11/// An implementation only ever reads this, so every increase in the simulated venue's fidelity —
12/// sizes at the touch, depth, a queue position — arrives as a new field rather than as a new
13/// parameter on [`fill_price`](FillModel::fill_price). That is what `#[non_exhaustive]` is for
14/// here: constructing one outside this crate would turn each such addition back into a breaking
15/// change, which is the churn this type exists to end.
16///
17/// The order's limit price is deliberately absent — see [`FillModel`].
18#[non_exhaustive]
19#[derive(Debug, Clone, Copy, Eq, PartialEq)]
20pub struct FillContext<'a> {
21    /// Which way the order trades.
22    pub side: Side,
23    /// How much the order asks to trade, which a size-aware impact model prices from.
24    ///
25    /// This is the order's whole quantity. Where the book shows less size than that, the venue
26    /// fills only what the book has and prices that part at what the model returns, so a model
27    /// reading this prices the order as if all of it traded.
28    pub quantity: Decimal,
29    /// The venue's view of the instrument. Every field is optional and absence is ordinary; see
30    /// [`MarketSnapshot`].
31    pub market: &'a MarketSnapshot,
32}
33
34impl<'a> FillContext<'a> {
35    pub fn new(side: Side, quantity: Decimal, market: &'a MarketSnapshot) -> Self {
36        Self {
37            side,
38            quantity,
39            market,
40        }
41    }
42}
43
44/// Prices a fill the venue has already decided will happen.
45///
46/// # Backtest-only
47///
48/// Only simulated execution consults a `FillModel`. Live execution clients receive real fill
49/// prices from the venue.
50///
51/// # Called only for a taker
52///
53/// A market order, or a limit order that is marketable when it arrives. A resting order matched
54/// later fills at *its own limit price* and never reaches a fill model: a maker is paid the price
55/// it quoted, and price improvement accrues to the aggressor that crossed it.
56///
57/// # This model does not see the limit price, and must not
58///
59/// A limit constrains the *result*, not the pricing. The venue clamps whatever is returned to the
60/// order's limit — never above a buy's, never below a sell's — so an implementation models
61/// slippage and spread and cannot produce a fill outside the order's terms.
62///
63/// Reading the limit here is how two of the three models this crate ships came to return the limit
64/// *itself* for a marketable order: a buy limit of 51,000 arriving against an ask of 50,000 filled
65/// at 51,000 — a worse price than a market order got under the same configuration. The third
66/// returned the midpoint even when it sat above a buy's limit. Removing the parameter is what
67/// makes that class of error unwritable rather than merely documented.
68///
69/// # Extensibility
70///
71/// Implement this to model market impact or other execution dynamics, and hand it to the venue
72/// with [`SimulatedVenue::with_fill_model`]. The built-in models ([`LastPriceFillModel`],
73/// [`BidAskFillModel`], [`MidpointFillModel`]) are baselines, and [`SimFillConfig`] selects one of
74/// them from a serialisable configuration.
75///
76/// Adverse slippage, fixed or drawn at random, needs no model of its own: the venue applies its
77/// [`SlippageConfig`] to whatever price the model returns, whichever model that is.
78///
79/// # It must be a pure function of its context
80///
81/// The venue may call [`fill_price`](Self::fill_price) more than once for one order, and for
82/// orders that never fill: it asks, for instance, whether a short sale is priceable at all before
83/// deciding to refuse it. So the same context must always give the same price, which is why this
84/// takes `&self`. Randomness belongs in the venue's [`SlippageConfig`], whose seeded stream
85/// advances exactly once per fill.
86///
87/// The price returned must be a real market price: the venue's arithmetic on it, slippage and
88/// settlement included, panics on `Decimal` overflow.
89///
90/// # It fills optimistically by default
91///
92/// The venue prices a taker at the market as it stands when the order arrives, which in a backtest
93/// on bars is the close of the bar the strategy has just seen. That is not look-ahead, since the
94/// close is known when deciding, but it is optimistic: it assumes trading at an observed price with
95/// no latency and, unless a model or slippage says otherwise, no cost of crossing.
96///
97/// # Returns
98///
99/// `None` when the context carries nothing this model can price from — no prices at all on the
100/// first ticks of a backtest, say. The venue turns that into a rejection naming the instrument
101/// rather than a panic.
102///
103/// [`SimulatedVenue::with_fill_model`]: crate::exchange::mock::SimulatedVenue::with_fill_model
104/// [`SlippageConfig`]: crate::slippage::SlippageConfig
105pub trait FillModel: Debug + Send + Sync {
106    fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal>;
107}
108
109/// Fills at the last traded price, falling back to the price a taker would have to cross to.
110///
111/// Fallback chain: `last_price` → `best_ask` (Buy) / `best_bid` (Sell).
112///
113/// The simplest fill model — it ignores the spread entirely. Useful when spread modelling is
114/// handled elsewhere, or when deterministic fills are preferred.
115///
116/// Note that with both sides of an L1 book present, `last_price` is the *microprice* rather than
117/// the most recent print; see [`MarketSnapshot`].
118#[derive(
119    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
120)]
121pub struct LastPriceFillModel;
122
123impl FillModel for LastPriceFillModel {
124    fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
125        fill.market.last_price.or(match fill.side {
126            Side::Buy => fill.market.best_ask,
127            Side::Sell => fill.market.best_bid,
128        })
129    }
130}
131
132/// Fills at the current best ask (buys) or best bid (sells), crossing the spread as a taker does.
133///
134/// Falls back to `last_price` when the relevant side of the book is absent. More realistic than
135/// [`LastPriceFillModel`] for strategies that frequently cross the spread.
136#[derive(
137    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
138)]
139pub struct BidAskFillModel;
140
141impl FillModel for BidAskFillModel {
142    fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
143        match fill.side {
144            Side::Buy => fill.market.best_ask.or(fill.market.last_price),
145            Side::Sell => fill.market.best_bid.or(fill.market.last_price),
146        }
147    }
148}
149
150/// Fills at the midpoint of best bid and best ask, falling back to `last_price`.
151///
152/// Useful when modelling execution quality between taker (crossing the spread) and maker (resting
153/// at the quote), or when both sides of the book are always present in the backtest feed.
154///
155/// # It is optimistic for a taker
156///
157/// A midpoint is not a price anyone is offering, so a crossing order modelled this way saves half
158/// the spread it would really have paid. The venue bounds the result by the order's own limit;
159/// nothing bounds it by the touch.
160#[derive(
161    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
162)]
163pub struct MidpointFillModel;
164
165impl FillModel for MidpointFillModel {
166    fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
167        fill.market.mid_price().or(fill.market.last_price)
168    }
169}
170
171/// Selects one of the built-in fill models from a serialisable configuration, as
172/// [`MockExecutionConfig::fill_model`] does. Defaults to [`LastPriceFillModel`].
173///
174/// A model of your own cannot be named here; hand it to the venue with
175/// [`SimulatedVenue::with_fill_model`] instead.
176///
177/// [`MockExecutionConfig::fill_model`]: crate::client::mock::MockExecutionConfig::fill_model
178/// [`SimulatedVenue::with_fill_model`]: crate::exchange::mock::SimulatedVenue::with_fill_model
179#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
180pub enum SimFillConfig {
181    LastPrice(LastPriceFillModel),
182    BidAsk(BidAskFillModel),
183    Midpoint(MidpointFillModel),
184}
185
186impl Default for SimFillConfig {
187    fn default() -> Self {
188        Self::LastPrice(LastPriceFillModel)
189    }
190}
191
192impl FillModel for SimFillConfig {
193    fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
194        match self {
195            SimFillConfig::LastPrice(model) => model.fill_price(fill),
196            SimFillConfig::BidAsk(model) => model.fill_price(fill),
197            SimFillConfig::Midpoint(model) => model.fill_price(fill),
198        }
199    }
200}
201
202/// When a simulated venue fills a market order, as [`MockExecutionConfig::market_order_timing`]
203/// selects. Defaults to [`OnArrival`](Self::OnArrival).
204///
205/// `#[non_exhaustive]`: other rules for when a held order is released may be added.
206///
207/// [`MockExecutionConfig::market_order_timing`]: crate::client::mock::MockExecutionConfig::market_order_timing
208#[derive(
209    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
210)]
211#[non_exhaustive]
212pub enum MarketOrderTiming {
213    /// Fill the moment the order reaches the venue, against the market it holds then.
214    ///
215    /// On a bar feed with no latency, that market is the close of the bar the sender has just
216    /// seen: the order trades at exactly the price it decided on, with no time to react, no
217    /// spread crossed and no impact. A common convention, and an optimistic one — a strategy can
218    /// come to depend on it. [`NextBar`](Self::NextBar) is the pessimistic comparison.
219    #[default]
220    OnArrival,
221
222    /// Hold the order until its instrument's next bar, and fill it at that bar's price.
223    ///
224    /// For bar data only: the venue must be fed bars, through the `bar` argument of
225    /// [`SimulatedVenue::apply_market`](crate::exchange::mock::SimulatedVenue::apply_market).
226    /// Anything else fails visibly rather than falling back:
227    /// - a market order on an instrument the venue has never been fed a bar for is rejected, naming
228    ///   this setting. That includes every market order on a
229    ///   [`RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced) venue, which
230    ///   has no feed at all;
231    /// - a tick or quote between two bars moves the market but does not release a held order.
232    ///
233    /// # While it is held
234    /// The order is accepted as `Open`, so a cancel takes it back as it would a resting order. The
235    /// venue holds what the order would cost at its current price, as it holds a limit order's
236    /// cost at its limit, so two held orders cannot both spend one balance. An order the account
237    /// cannot cover at that price is refused on arrival, as it is under
238    /// [`OnArrival`](Self::OnArrival).
239    ///
240    /// # When the next bar arrives
241    /// The hold is released and the order filled as a taker, at the reference price
242    /// [`NextBarPrice`] picks, priced by the venue's [`FillModel`] and moved by its slippage like
243    /// any other taker fill. The fill is costed afresh, and an order the account can no longer
244    /// afford is cancelled and reported. Its time in force decides the rest as on arrival:
245    /// immediate-or-cancel keeps what the bar's size allows and cancels the remainder,
246    /// fill-or-kill trades all of it or nothing. The fill is stamped at the bar's instant.
247    ///
248    /// # When the data ends first
249    /// An order still held when its driver runs out of data is retired as expired, filled
250    /// nothing, so every accepted order is answered.
251    ///
252    /// Only market orders are held for the bar. A limit order rests and matches as it always does,
253    /// unless [`StaleMarketPolicy::Wait`] holds it on a stale market.
254    ///
255    /// Any bar releases a held order, one reported with zero volume included. To have such a bar
256    /// release nothing, combine this with [`StaleMarketPolicy::Wait`].
257    NextBar(NextBarPrice),
258}
259
260/// Which price of the next bar fills a market order held for it. See
261/// [`MarketOrderTiming::NextBar`].
262///
263/// `#[non_exhaustive]`: other reference prices may be added.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
265#[non_exhaustive]
266pub enum NextBarPrice {
267    /// The next bar's open: the first price the market traded at after the order was sent. The
268    /// realistic choice.
269    Open,
270
271    /// The next bar's worst price for the order: its high for a buy, its low for a sell.
272    ///
273    /// A deliberately **pessimistic bound**, not a model. A market order fills near the start of
274    /// the next bar, not at its extreme, so this overstates the cost of trading, and the more so
275    /// the longer or more volatile the bar. Its use is as a stress test: a strategy that still
276    /// makes money under it does not owe its edge to fill quality. It already charges more than
277    /// any slippage would, so combining it with slippage counts the same cost twice.
278    AdverseExtreme,
279}
280
281/// What a simulated venue does with an order when the instrument's market is stale, as
282/// [`MockExecutionConfig::stale_market`] selects. Defaults to [`Off`](Self::Off).
283///
284/// # The problem it addresses
285/// A venue fed a market keeps the last one it saw for each instrument, and by default fills an
286/// order against it however old it is. On a bar feed, a market order on an instrument with no bar
287/// at the current instant therefore fills at the close of its last bar, which may be minutes, hours
288/// or a weekend old, and so does a limit order that close crosses. A real order would not get that
289/// price: in a quiet market it trades against the book, and in an outage, a halt or a closed market
290/// it is refused or waits for the reopen, filling at a price that may have gapped. A bar that only
291/// carries the previous close forward, with zero volume, makes this worse: it looks recent, but
292/// nothing traded.
293///
294/// A venue cannot tell a quiet market from a dead feed or a halt by looking at bars, and this
295/// policy does not try to. It treats an old market as one nobody can trade in yet.
296///
297/// # When to turn it on, and why it is off by default
298/// Turn it on for bar feeds, with a `max_age` a little over the bar interval, so that only a
299/// market with a missing bar counts as stale. It stays off by default because a venue cannot know
300/// a feed's bar interval, and because on a quote or tick feed the book stays valid until it is
301/// replaced: a quiet book is still a book an order can trade against.
302///
303/// `#[non_exhaustive]`: other actions on a stale market may be added.
304///
305/// [`MockExecutionConfig::stale_market`]: crate::client::mock::MockExecutionConfig::stale_market
306#[derive(
307    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
308)]
309#[non_exhaustive]
310pub enum StaleMarketPolicy {
311    /// Fill an order against the market the venue holds, however old.
312    #[default]
313    Off,
314
315    /// An order arriving when the instrument's last fresh observation is more than `max_age` old
316    /// waits for the next one, and an observation that is not fresh fills no resting order.
317    ///
318    /// # What is fresh
319    /// An observation the driver marks [`fresh`](crate::market::MarketObservation::fresh): a
320    /// trade, a quote, or a bar something traded in. A bar reported with zero volume still moves
321    /// the price the venue holds, but not the instant the market's age is measured from. An
322    /// observation exactly `max_age` old is still fresh, so a `max_age` of zero accepts an order
323    /// only at the instant of a fresh observation.
324    ///
325    /// An instrument whose venue has no price at all is not stale but unpriced: its market orders
326    /// are rejected and its limit orders rest, as they do without this policy.
327    ///
328    /// # A feed reported down
329    /// Age alone cannot tell a quiet market from a dead feed. A driver that learns its feed has
330    /// stopped delivering an instrument, because the stream is reconnecting or has ended, says so
331    /// with
332    /// [`SimulatedVenue::mark_market_stale`](crate::exchange::mock::SimulatedVenue::mark_market_stale).
333    /// The market then counts as stale, however recently it was observed fresh, until its next
334    /// fresh observation. The `rustrade` crate's paper venues and `SimRunner` do this for every
335    /// instrument priced from an exchange whose stream reconnects, and paper venues also for every
336    /// instrument when the stream ends. `SimRunner` marks nothing when its data ends, since nothing
337    /// arrives after it. A driver that never calls it leaves this policy judging by age alone,
338    /// which is all it can do for a connection that stays open but goes silent.
339    ///
340    /// # What waits
341    /// Every order arriving on a stale market, market and limit alike: whether the old market
342    /// crosses a limit order says nothing about whether the market it will trade in does. The
343    /// order is accepted as `Open` and held as [`MarketOrderTiming::NextBar`] holds a market
344    /// order: a cancel takes it back, and the venue holds what it would cost arriving now. That
345    /// is the current price for a market order, capped at the limit for a limit order that price
346    /// crosses, and the limit for one it does not cross. An order the account cannot cover is
347    /// refused on arrival. While it is held, nothing fills it: not even an observation that
348    /// crosses its limit, until that observation is fresh.
349    ///
350    /// An immediate-or-cancel or fill-or-kill order is rejected instead, with a reason naming
351    /// this policy: it asked to trade now or not at all. That includes the immediate-or-cancel
352    /// orders an engine sends to close positions, so a strategy closing out on a stale instrument
353    /// has to try again once its market is fresh. An order already past its own deadline expires,
354    /// as it would on a fresh market.
355    ///
356    /// # When it fills
357    /// On the instrument's next fresh observation, as if it had arrived just after it, to the
358    /// market that observation leaves: a trade's or quote's price, or a bar's close.
359    /// - A market order is priced by the venue's [`FillModel`] from that market, moved by its
360    ///   slippage, and costed afresh, as any taker fill is.
361    /// - A limit order that market crosses trades the same way, capped at its limit, up to the
362    ///   size the market has, and the rest of it joins the book at its limit. One that market does
363    ///   not cross joins the book whole, at its limit. A post-only one that market crosses is
364    ///   cancelled, as it would have been on arrival.
365    ///
366    /// A fill is stamped at the observation's instant and follows the fills of orders already
367    /// resting, which the observation crossed first; a limit order joining the book then is not
368    /// crossed by the observation that released it. An order the account can no longer afford, or
369    /// a market order that the market cannot price, is cancelled and reported.
370    ///
371    /// Only an observation of that instrument releases a held order: a clock moving, or a fresh
372    /// observation of another instrument, never does. A deadline the order carries still retires
373    /// it, and an order still waiting when its driver runs out of data is retired as expired,
374    /// filled nothing.
375    ///
376    /// # Resting orders
377    /// A limit order already resting is filled at its limit when a fresh observation crosses it,
378    /// as without this policy. An observation that is not fresh, such as a bar reported with zero
379    /// volume, still moves the price the venue holds but fills no resting order, however far it
380    /// moves: it is not evidence that anything traded there.
381    ///
382    /// # With [`MarketOrderTiming::NextBar`]
383    /// Every market order already waits for its instrument's next bar and fills at that bar's
384    /// price, so a fresh market changes nothing for one. A limit order arriving on a stale market
385    /// waits for the next bar too, and is decided against that bar's price as a market order is
386    /// priced from it. What it does not trade there rests at its limit, where the rest of that bar
387    /// can cross it as it crosses any resting order. On a fresh market a limit order is decided on
388    /// arrival, as without this policy. A bar reported with zero volume releases nothing: the
389    /// orders held for it wait for the next bar something traded in, except an immediate-or-cancel
390    /// or fill-or-kill order, which the bar cancels, since it had nothing to trade. An
391    /// immediate-or-cancel or fill-or-kill order arriving on a stale market is rejected, as above.
392    ///
393    /// # Needs a market feed
394    /// A [`RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced) venue, the
395    /// kind `MockExchange` drives, has no market feed to measure an age from, so it rejects every
396    /// market order, naming this policy (it accepts no limit order at all). On a venue that is fed
397    /// a market, an instrument it has never been fed has no price at all, and its market orders are
398    /// rejected as unpriced, as above.
399    ///
400    /// # Serialised
401    /// `max_age` is a [`Duration`], so it serialises as seconds and nanoseconds:
402    /// `{"Wait":{"max_age":{"secs":90,"nanos":0}}}`.
403    Wait {
404        /// How old the last fresh observation may be for an order to be decided on arrival.
405        max_age: Duration,
406    },
407}
408
409impl NextBarPrice {
410    /// The reference price `bar` gives an order on `side`.
411    pub fn reference(self, side: Side, bar: &MarketBar) -> Decimal {
412        match (self, side) {
413            (Self::Open, _) => bar.open,
414            (Self::AdverseExtreme, Side::Buy) => bar.high,
415            (Self::AdverseExtreme, Side::Sell) => bar.low,
416        }
417    }
418}
419
420#[cfg(test)]
421#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
422mod tests {
423    use super::*;
424
425    #[test]
426    fn the_next_bar_price_is_its_open_or_its_worst_for_the_side() {
427        let bar = MarketBar::new(d("100"), d("110"), d("90"), d("105"));
428
429        assert_eq!(NextBarPrice::Open.reference(Side::Buy, &bar), d("100"));
430        assert_eq!(NextBarPrice::Open.reference(Side::Sell, &bar), d("100"));
431        assert_eq!(
432            NextBarPrice::AdverseExtreme.reference(Side::Buy, &bar),
433            d("110")
434        );
435        assert_eq!(
436            NextBarPrice::AdverseExtreme.reference(Side::Sell, &bar),
437            d("90")
438        );
439    }
440
441    #[test]
442    fn market_order_timing_defaults_to_on_arrival_and_round_trips() {
443        assert_eq!(MarketOrderTiming::default(), MarketOrderTiming::OnArrival);
444
445        for timing in [
446            MarketOrderTiming::OnArrival,
447            MarketOrderTiming::NextBar(NextBarPrice::Open),
448            MarketOrderTiming::NextBar(NextBarPrice::AdverseExtreme),
449        ] {
450            let json = serde_json::to_string(&timing).unwrap();
451            assert_eq!(
452                serde_json::from_str::<MarketOrderTiming>(&json).unwrap(),
453                timing
454            );
455        }
456    }
457
458    #[test]
459    fn stale_market_policy_defaults_to_off_and_round_trips() {
460        assert_eq!(StaleMarketPolicy::default(), StaleMarketPolicy::Off);
461
462        let wait = StaleMarketPolicy::Wait {
463            max_age: Duration::from_secs(90),
464        };
465        assert_eq!(
466            serde_json::to_string(&wait).unwrap(),
467            r#"{"Wait":{"max_age":{"secs":90,"nanos":0}}}"#,
468            "the shape the rustdoc promises a configuration file"
469        );
470
471        for policy in [StaleMarketPolicy::Off, wait] {
472            let json = serde_json::to_string(&policy).unwrap();
473            assert_eq!(
474                serde_json::from_str::<StaleMarketPolicy>(&json).unwrap(),
475                policy
476            );
477        }
478    }
479
480    fn d(s: &str) -> Decimal {
481        s.parse().unwrap()
482    }
483
484    /// Bid 99.5 / ask 100.5 / last 100.0 — a complete snapshot, mid also 100.0.
485    fn prices() -> MarketSnapshot {
486        MarketSnapshot::new(Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
487    }
488
489    /// A snapshot from three literal prices, for the partial-book cases.
490    fn snapshot(
491        bid: Option<Decimal>,
492        ask: Option<Decimal>,
493        last: Option<Decimal>,
494    ) -> MarketSnapshot {
495        MarketSnapshot::new(bid, ask, last)
496    }
497
498    #[test]
499    fn last_price_market_buy_uses_last() {
500        let market = prices();
501        assert_eq!(
502            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
503            Some(d("100.0"))
504        );
505    }
506
507    #[test]
508    fn bid_ask_market_buy_uses_ask() {
509        let market = prices();
510        assert_eq!(
511            BidAskFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
512            Some(d("100.5"))
513        );
514    }
515
516    #[test]
517    fn bid_ask_market_sell_uses_bid() {
518        let market = prices();
519        assert_eq!(
520            BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
521            Some(d("99.5"))
522        );
523    }
524
525    #[test]
526    fn midpoint_uses_mid() {
527        let market = prices();
528        assert_eq!(
529            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
530            Some(d("100.0"))
531        );
532    }
533
534    #[test]
535    fn midpoint_falls_back_to_last_when_no_bid_ask() {
536        assert_eq!(
537            MidpointFillModel.fill_price(&FillContext::new(
538                Side::Buy,
539                Decimal::ONE,
540                &snapshot(None, None, Some(d("100.0")))
541            )),
542            Some(d("100.0"))
543        );
544    }
545
546    // --- SimFillConfig enum dispatch ---
547
548    #[test]
549    fn fill_model_config_last_price_dispatches() {
550        let market = prices();
551        let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
552        assert_eq!(
553            cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
554            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
555        );
556    }
557
558    #[test]
559    fn fill_model_config_bid_ask_dispatches() {
560        let market = prices();
561        let cfg = SimFillConfig::BidAsk(BidAskFillModel);
562        assert_eq!(
563            cfg.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
564            BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market))
565        );
566    }
567
568    #[test]
569    fn fill_model_config_midpoint_dispatches() {
570        let market = prices();
571        let cfg = SimFillConfig::Midpoint(MidpointFillModel);
572        assert_eq!(
573            cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
574            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
575        );
576    }
577
578    #[test]
579    fn fill_model_config_default_is_last_price() {
580        assert_eq!(
581            SimFillConfig::default(),
582            SimFillConfig::LastPrice(LastPriceFillModel)
583        );
584    }
585
586    // --- Edge cases ---
587
588    #[test]
589    fn last_price_all_none_returns_none() {
590        // No market data at all — e.g. first tick of a backtest before any prices arrive.
591        // The mock exchange falls back to request.state.price when fill_price returns None.
592        assert_eq!(
593            LastPriceFillModel.fill_price(&FillContext::new(
594                Side::Buy,
595                Decimal::ONE,
596                &snapshot(None, None, None)
597            )),
598            None
599        );
600        assert_eq!(
601            LastPriceFillModel.fill_price(&FillContext::new(
602                Side::Sell,
603                Decimal::ONE,
604                &snapshot(None, None, None)
605            )),
606            None
607        );
608    }
609
610    #[test]
611    fn last_price_falls_back_to_bid_ask_when_no_last_price() {
612        // When last_price=None but bid/ask are present, the model falls back to
613        // bid/ask (as documented in the fallback chain). This exercises the tertiary
614        // fallback that was previously untested.
615        let market = snapshot(Some(d("99.5")), Some(d("100.5")), None);
616        assert_eq!(
617            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
618            Some(d("100.5")),
619            "Buy with no last_price should fall back to best_ask"
620        );
621        assert_eq!(
622            LastPriceFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
623            Some(d("99.5")),
624            "Sell with no last_price should fall back to best_bid"
625        );
626    }
627
628    /// A partial book falls back to the last price, whichever side is missing.
629    ///
630    /// These replace a pair that asserted the *limit* beat a stale `last_price` on a partial book.
631    /// That special case existed only to stop a limit buy filling above its own limit, which is now
632    /// the venue's job — and doing it here was what let the complete-book arm keep filling above a
633    /// limit unnoticed.
634    #[test]
635    fn midpoint_with_only_bid_falls_back_to_last() {
636        assert_eq!(
637            MidpointFillModel.fill_price(&FillContext::new(
638                Side::Buy,
639                Decimal::ONE,
640                &snapshot(Some(d("99.5")), None, Some(d("100.0")))
641            )),
642            Some(d("100.0"))
643        );
644    }
645
646    #[test]
647    fn midpoint_with_only_ask_falls_back_to_last() {
648        assert_eq!(
649            MidpointFillModel.fill_price(&FillContext::new(
650                Side::Sell,
651                Decimal::ONE,
652                &snapshot(None, Some(d("100.5")), Some(d("100.0")))
653            )),
654            Some(d("100.0"))
655        );
656    }
657
658    /// A complete book prices at the midpoint even where that sits far from the last print.
659    ///
660    /// This case had no coverage at all, and it is where the model used to fill a limit buy above
661    /// its own limit: `mid_price()` won outright and the limit was never consulted. There is no
662    /// limit to consult now — bounding the result is the venue's job — so what this pins is that
663    /// the midpoint wins over `last_price`, which is the whole content of the model.
664    #[test]
665    fn midpoint_complete_book_prices_at_mid_not_last() {
666        let market = snapshot(Some(d("99.5")), Some(d("100.5")), Some(d("110.0")));
667        assert_eq!(
668            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
669            Some(d("100.0")),
670            "the midpoint of 99.5/100.5, not the stale 110.0 print"
671        );
672    }
673
674    /// No model reads the side it is not given, and none invents a price from nothing.
675    #[test]
676    fn every_model_returns_none_on_an_empty_snapshot() {
677        let empty = MarketSnapshot::default();
678        for side in [Side::Buy, Side::Sell] {
679            let fill = FillContext::new(side, Decimal::ONE, &empty);
680            assert_eq!(LastPriceFillModel.fill_price(&fill), None);
681            assert_eq!(BidAskFillModel.fill_price(&fill), None);
682            assert_eq!(MidpointFillModel.fill_price(&fill), None);
683        }
684    }
685}