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    /// # What waits
329    /// Every order arriving on a stale market, market and limit alike: whether the old market
330    /// crosses a limit order says nothing about whether the market it will trade in does. The
331    /// order is accepted as `Open` and held as [`MarketOrderTiming::NextBar`] holds a market
332    /// order: a cancel takes it back, and the venue holds what it would cost arriving now. That
333    /// is the current price for a market order, capped at the limit for a limit order that price
334    /// crosses, and the limit for one it does not cross. An order the account cannot cover is
335    /// refused on arrival. While it is held, nothing fills it: not even an observation that
336    /// crosses its limit, until that observation is fresh.
337    ///
338    /// An immediate-or-cancel or fill-or-kill order is rejected instead, with a reason naming
339    /// this policy: it asked to trade now or not at all. That includes the immediate-or-cancel
340    /// orders an engine sends to close positions, so a strategy closing out on a stale instrument
341    /// has to try again once its market is fresh. An order already past its own deadline expires,
342    /// as it would on a fresh market.
343    ///
344    /// # When it fills
345    /// On the instrument's next fresh observation, as if it had arrived just after it, to the
346    /// market that observation leaves: a trade's or quote's price, or a bar's close.
347    /// - A market order is priced by the venue's [`FillModel`] from that market, moved by its
348    ///   slippage, and costed afresh, as any taker fill is.
349    /// - A limit order that market crosses trades the same way, capped at its limit, up to the
350    ///   size the market has, and the rest of it joins the book at its limit. One that market does
351    ///   not cross joins the book whole, at its limit. A post-only one that market crosses is
352    ///   cancelled, as it would have been on arrival.
353    ///
354    /// A fill is stamped at the observation's instant and follows the fills of orders already
355    /// resting, which the observation crossed first; a limit order joining the book then is not
356    /// crossed by the observation that released it. An order the account can no longer afford, or
357    /// a market order that the market cannot price, is cancelled and reported.
358    ///
359    /// Only an observation of that instrument releases a held order: a clock moving, or a fresh
360    /// observation of another instrument, never does. A deadline the order carries still retires
361    /// it, and an order still waiting when its driver runs out of data is retired as expired,
362    /// filled nothing.
363    ///
364    /// # Resting orders
365    /// A limit order already resting is filled at its limit when a fresh observation crosses it,
366    /// as without this policy. An observation that is not fresh, such as a bar reported with zero
367    /// volume, still moves the price the venue holds but fills no resting order, however far it
368    /// moves: it is not evidence that anything traded there.
369    ///
370    /// # With [`MarketOrderTiming::NextBar`]
371    /// Every market order already waits for its instrument's next bar and fills at that bar's
372    /// price, so a fresh market changes nothing for one. A limit order arriving on a stale market
373    /// waits for the next bar too, and is decided against that bar's price as a market order is
374    /// priced from it. What it does not trade there rests at its limit, where the rest of that bar
375    /// can cross it as it crosses any resting order. On a fresh market a limit order is decided on
376    /// arrival, as without this policy. A bar reported with zero volume releases nothing: the
377    /// orders held for it wait for the next bar something traded in, except an immediate-or-cancel
378    /// or fill-or-kill order, which the bar cancels, since it had nothing to trade. An
379    /// immediate-or-cancel or fill-or-kill order arriving on a stale market is rejected, as above.
380    ///
381    /// # Needs a market feed
382    /// A [`RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced) venue, the
383    /// kind `MockExchange` drives, has no market feed to measure an age from, so it rejects every
384    /// market order, naming this policy (it accepts no limit order at all). On a venue that is fed
385    /// a market, an instrument it has never been fed has no price at all, and its market orders are
386    /// rejected as unpriced, as above.
387    ///
388    /// # Serialised
389    /// `max_age` is a [`Duration`], so it serialises as seconds and nanoseconds:
390    /// `{"Wait":{"max_age":{"secs":90,"nanos":0}}}`.
391    Wait {
392        /// How old the last fresh observation may be for an order to be decided on arrival.
393        max_age: Duration,
394    },
395}
396
397impl NextBarPrice {
398    /// The reference price `bar` gives an order on `side`.
399    pub fn reference(self, side: Side, bar: &MarketBar) -> Decimal {
400        match (self, side) {
401            (Self::Open, _) => bar.open,
402            (Self::AdverseExtreme, Side::Buy) => bar.high,
403            (Self::AdverseExtreme, Side::Sell) => bar.low,
404        }
405    }
406}
407
408#[cfg(test)]
409#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
410mod tests {
411    use super::*;
412
413    #[test]
414    fn the_next_bar_price_is_its_open_or_its_worst_for_the_side() {
415        let bar = MarketBar::new(d("100"), d("110"), d("90"), d("105"));
416
417        assert_eq!(NextBarPrice::Open.reference(Side::Buy, &bar), d("100"));
418        assert_eq!(NextBarPrice::Open.reference(Side::Sell, &bar), d("100"));
419        assert_eq!(
420            NextBarPrice::AdverseExtreme.reference(Side::Buy, &bar),
421            d("110")
422        );
423        assert_eq!(
424            NextBarPrice::AdverseExtreme.reference(Side::Sell, &bar),
425            d("90")
426        );
427    }
428
429    #[test]
430    fn market_order_timing_defaults_to_on_arrival_and_round_trips() {
431        assert_eq!(MarketOrderTiming::default(), MarketOrderTiming::OnArrival);
432
433        for timing in [
434            MarketOrderTiming::OnArrival,
435            MarketOrderTiming::NextBar(NextBarPrice::Open),
436            MarketOrderTiming::NextBar(NextBarPrice::AdverseExtreme),
437        ] {
438            let json = serde_json::to_string(&timing).unwrap();
439            assert_eq!(
440                serde_json::from_str::<MarketOrderTiming>(&json).unwrap(),
441                timing
442            );
443        }
444    }
445
446    #[test]
447    fn stale_market_policy_defaults_to_off_and_round_trips() {
448        assert_eq!(StaleMarketPolicy::default(), StaleMarketPolicy::Off);
449
450        let wait = StaleMarketPolicy::Wait {
451            max_age: Duration::from_secs(90),
452        };
453        assert_eq!(
454            serde_json::to_string(&wait).unwrap(),
455            r#"{"Wait":{"max_age":{"secs":90,"nanos":0}}}"#,
456            "the shape the rustdoc promises a configuration file"
457        );
458
459        for policy in [StaleMarketPolicy::Off, wait] {
460            let json = serde_json::to_string(&policy).unwrap();
461            assert_eq!(
462                serde_json::from_str::<StaleMarketPolicy>(&json).unwrap(),
463                policy
464            );
465        }
466    }
467
468    fn d(s: &str) -> Decimal {
469        s.parse().unwrap()
470    }
471
472    /// Bid 99.5 / ask 100.5 / last 100.0 — a complete snapshot, mid also 100.0.
473    fn prices() -> MarketSnapshot {
474        MarketSnapshot::new(Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
475    }
476
477    /// A snapshot from three literal prices, for the partial-book cases.
478    fn snapshot(
479        bid: Option<Decimal>,
480        ask: Option<Decimal>,
481        last: Option<Decimal>,
482    ) -> MarketSnapshot {
483        MarketSnapshot::new(bid, ask, last)
484    }
485
486    #[test]
487    fn last_price_market_buy_uses_last() {
488        let market = prices();
489        assert_eq!(
490            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
491            Some(d("100.0"))
492        );
493    }
494
495    #[test]
496    fn bid_ask_market_buy_uses_ask() {
497        let market = prices();
498        assert_eq!(
499            BidAskFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
500            Some(d("100.5"))
501        );
502    }
503
504    #[test]
505    fn bid_ask_market_sell_uses_bid() {
506        let market = prices();
507        assert_eq!(
508            BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
509            Some(d("99.5"))
510        );
511    }
512
513    #[test]
514    fn midpoint_uses_mid() {
515        let market = prices();
516        assert_eq!(
517            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
518            Some(d("100.0"))
519        );
520    }
521
522    #[test]
523    fn midpoint_falls_back_to_last_when_no_bid_ask() {
524        assert_eq!(
525            MidpointFillModel.fill_price(&FillContext::new(
526                Side::Buy,
527                Decimal::ONE,
528                &snapshot(None, None, Some(d("100.0")))
529            )),
530            Some(d("100.0"))
531        );
532    }
533
534    // --- SimFillConfig enum dispatch ---
535
536    #[test]
537    fn fill_model_config_last_price_dispatches() {
538        let market = prices();
539        let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
540        assert_eq!(
541            cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
542            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
543        );
544    }
545
546    #[test]
547    fn fill_model_config_bid_ask_dispatches() {
548        let market = prices();
549        let cfg = SimFillConfig::BidAsk(BidAskFillModel);
550        assert_eq!(
551            cfg.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
552            BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market))
553        );
554    }
555
556    #[test]
557    fn fill_model_config_midpoint_dispatches() {
558        let market = prices();
559        let cfg = SimFillConfig::Midpoint(MidpointFillModel);
560        assert_eq!(
561            cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
562            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
563        );
564    }
565
566    #[test]
567    fn fill_model_config_default_is_last_price() {
568        assert_eq!(
569            SimFillConfig::default(),
570            SimFillConfig::LastPrice(LastPriceFillModel)
571        );
572    }
573
574    // --- Edge cases ---
575
576    #[test]
577    fn last_price_all_none_returns_none() {
578        // No market data at all — e.g. first tick of a backtest before any prices arrive.
579        // The mock exchange falls back to request.state.price when fill_price returns None.
580        assert_eq!(
581            LastPriceFillModel.fill_price(&FillContext::new(
582                Side::Buy,
583                Decimal::ONE,
584                &snapshot(None, None, None)
585            )),
586            None
587        );
588        assert_eq!(
589            LastPriceFillModel.fill_price(&FillContext::new(
590                Side::Sell,
591                Decimal::ONE,
592                &snapshot(None, None, None)
593            )),
594            None
595        );
596    }
597
598    #[test]
599    fn last_price_falls_back_to_bid_ask_when_no_last_price() {
600        // When last_price=None but bid/ask are present, the model falls back to
601        // bid/ask (as documented in the fallback chain). This exercises the tertiary
602        // fallback that was previously untested.
603        let market = snapshot(Some(d("99.5")), Some(d("100.5")), None);
604        assert_eq!(
605            LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
606            Some(d("100.5")),
607            "Buy with no last_price should fall back to best_ask"
608        );
609        assert_eq!(
610            LastPriceFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
611            Some(d("99.5")),
612            "Sell with no last_price should fall back to best_bid"
613        );
614    }
615
616    /// A partial book falls back to the last price, whichever side is missing.
617    ///
618    /// These replace a pair that asserted the *limit* beat a stale `last_price` on a partial book.
619    /// That special case existed only to stop a limit buy filling above its own limit, which is now
620    /// the venue's job — and doing it here was what let the complete-book arm keep filling above a
621    /// limit unnoticed.
622    #[test]
623    fn midpoint_with_only_bid_falls_back_to_last() {
624        assert_eq!(
625            MidpointFillModel.fill_price(&FillContext::new(
626                Side::Buy,
627                Decimal::ONE,
628                &snapshot(Some(d("99.5")), None, Some(d("100.0")))
629            )),
630            Some(d("100.0"))
631        );
632    }
633
634    #[test]
635    fn midpoint_with_only_ask_falls_back_to_last() {
636        assert_eq!(
637            MidpointFillModel.fill_price(&FillContext::new(
638                Side::Sell,
639                Decimal::ONE,
640                &snapshot(None, Some(d("100.5")), Some(d("100.0")))
641            )),
642            Some(d("100.0"))
643        );
644    }
645
646    /// A complete book prices at the midpoint even where that sits far from the last print.
647    ///
648    /// This case had no coverage at all, and it is where the model used to fill a limit buy above
649    /// its own limit: `mid_price()` won outright and the limit was never consulted. There is no
650    /// limit to consult now — bounding the result is the venue's job — so what this pins is that
651    /// the midpoint wins over `last_price`, which is the whole content of the model.
652    #[test]
653    fn midpoint_complete_book_prices_at_mid_not_last() {
654        let market = snapshot(Some(d("99.5")), Some(d("100.5")), Some(d("110.0")));
655        assert_eq!(
656            MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
657            Some(d("100.0")),
658            "the midpoint of 99.5/100.5, not the stale 110.0 print"
659        );
660    }
661
662    /// No model reads the side it is not given, and none invents a price from nothing.
663    #[test]
664    fn every_model_returns_none_on_an_empty_snapshot() {
665        let empty = MarketSnapshot::default();
666        for side in [Side::Buy, Side::Sell] {
667            let fill = FillContext::new(side, Decimal::ONE, &empty);
668            assert_eq!(LastPriceFillModel.fill_price(&fill), None);
669            assert_eq!(BidAskFillModel.fill_price(&fill), None);
670            assert_eq!(MidpointFillModel.fill_price(&fill), None);
671        }
672    }
673}