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. A limit order rests and matches as it always does.
253 ///
254 /// Any bar releases a held order, one reported with zero volume included. To have such a bar
255 /// release nothing, combine this with [`StaleMarketPolicy::Wait`].
256 NextBar(NextBarPrice),
257}
258
259/// Which price of the next bar fills a market order held for it. See
260/// [`MarketOrderTiming::NextBar`].
261///
262/// `#[non_exhaustive]`: other reference prices may be added.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
264#[non_exhaustive]
265pub enum NextBarPrice {
266 /// The next bar's open: the first price the market traded at after the order was sent. The
267 /// realistic choice.
268 Open,
269
270 /// The next bar's worst price for the order: its high for a buy, its low for a sell.
271 ///
272 /// A deliberately **pessimistic bound**, not a model. A market order fills near the start of
273 /// the next bar, not at its extreme, so this overstates the cost of trading, and the more so
274 /// the longer or more volatile the bar. Its use is as a stress test: a strategy that still
275 /// makes money under it does not owe its edge to fill quality. It already charges more than
276 /// any slippage would, so combining it with slippage counts the same cost twice.
277 AdverseExtreme,
278}
279
280/// What a simulated venue does with a market order when the instrument's market is stale, as
281/// [`MockExecutionConfig::stale_market`] selects. Defaults to [`Off`](Self::Off).
282///
283/// # The problem it addresses
284/// A venue fed a market keeps the last one it saw for each instrument, and by default fills a
285/// market order against it however old it is. On a bar feed, a market order on an instrument with
286/// no bar at the current instant therefore fills at the close of its last bar, which may be
287/// minutes, hours or a weekend old. A real order would not get that price: in a quiet market it
288/// trades against the book, and in an outage, a halt or a closed market it is refused or waits
289/// for the reopen, filling at a price that may have gapped. A bar that only carries the previous
290/// close forward, with zero volume, makes this worse: it looks recent, but nothing traded.
291///
292/// A venue cannot tell a quiet market from a dead feed or a halt by looking at bars, and this
293/// policy does not try to. It treats an old market as one nobody can trade in yet.
294///
295/// # When to turn it on, and why it is off by default
296/// Turn it on for bar feeds, with a `max_age` a little over the bar interval, so that only a
297/// market with a missing bar counts as stale. It stays off by default because a venue cannot know
298/// a feed's bar interval, and because on a quote or tick feed the book stays valid until it is
299/// replaced: a quiet book is still a book a market order can trade against.
300///
301/// `#[non_exhaustive]`: other actions on a stale market may be added.
302///
303/// [`MockExecutionConfig::stale_market`]: crate::client::mock::MockExecutionConfig::stale_market
304#[derive(
305 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
306)]
307#[non_exhaustive]
308pub enum StaleMarketPolicy {
309 /// Fill a market order against the market the venue holds, however old.
310 #[default]
311 Off,
312
313 /// A market order arriving when the instrument's last fresh observation is more than
314 /// `max_age` old waits for the next one.
315 ///
316 /// # What is fresh
317 /// An observation the driver marks [`fresh`](crate::market::MarketObservation::fresh): a
318 /// trade, a quote, or a bar something traded in. A bar reported with zero volume still moves
319 /// the price the venue holds, but not the instant the market's age is measured from. An
320 /// observation exactly `max_age` old is still fresh, so a `max_age` of zero accepts an order
321 /// only at the instant of a fresh observation.
322 ///
323 /// An instrument whose venue has no price at all is not stale but unpriced, and its market
324 /// orders are rejected as they are without this policy.
325 ///
326 /// # What waits
327 /// Market orders only. The order is accepted as `Open` and held as
328 /// [`MarketOrderTiming::NextBar`] holds one: the venue holds what it would cost at the current
329 /// price, a cancel takes it back, and an order the account cannot cover at that price is
330 /// refused on arrival. An immediate-or-cancel or fill-or-kill market order is rejected
331 /// instead, with a reason naming this policy: it asked to trade now or not at all. That
332 /// includes the immediate-or-cancel orders an engine sends to close positions, so a strategy
333 /// closing out on a stale instrument has to try again once its market is fresh.
334 ///
335 /// A limit order that is marketable on arrival is not covered: it still fills against the
336 /// market the venue holds, however old.
337 ///
338 /// # When it fills
339 /// On the instrument's next fresh observation, as if it had arrived just after it: priced by
340 /// the venue's [`FillModel`] from the market that observation leaves, moved by its slippage,
341 /// and costed afresh, as any taker fill is. That is a trade's or quote's price, or a bar's
342 /// close. The fill is stamped at the observation's instant and follows the fills of orders
343 /// already resting, which the observation crossed first. An order the account can no longer
344 /// afford, or that the market cannot price, is cancelled and reported.
345 ///
346 /// Only an observation of that instrument releases a held order: a clock moving, or a fresh
347 /// observation of another instrument, never does. A deadline the order carries still retires
348 /// it, and an order still waiting when its driver runs out of data is retired as expired,
349 /// filled nothing.
350 ///
351 /// # With [`MarketOrderTiming::NextBar`]
352 /// Every market order already waits for its instrument's next bar and fills at that bar's
353 /// price, so a fresh market changes nothing. A bar reported with zero volume releases nothing:
354 /// the orders held for it wait for the next bar something traded in, except an
355 /// immediate-or-cancel or fill-or-kill order, which the bar cancels, since it had nothing to
356 /// trade. An immediate-or-cancel or fill-or-kill order arriving on a stale market is rejected,
357 /// as above.
358 ///
359 /// # Needs a market feed
360 /// A [`RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced) venue, the
361 /// kind `MockExchange` drives, has no market feed to measure an age from, so it rejects every
362 /// market order, naming this policy. On a venue that is fed a market, an instrument it has
363 /// never been fed has no price at all, and its market orders are rejected as unpriced, as
364 /// above.
365 ///
366 /// # Serialised
367 /// `max_age` is a [`Duration`], so it serialises as seconds and nanoseconds:
368 /// `{"Wait":{"max_age":{"secs":90,"nanos":0}}}`.
369 Wait {
370 /// How old the last fresh observation may be for a market order to fill on arrival.
371 max_age: Duration,
372 },
373}
374
375impl NextBarPrice {
376 /// The reference price `bar` gives an order on `side`.
377 pub fn reference(self, side: Side, bar: &MarketBar) -> Decimal {
378 match (self, side) {
379 (Self::Open, _) => bar.open,
380 (Self::AdverseExtreme, Side::Buy) => bar.high,
381 (Self::AdverseExtreme, Side::Sell) => bar.low,
382 }
383 }
384}
385
386#[cfg(test)]
387#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
388mod tests {
389 use super::*;
390
391 #[test]
392 fn the_next_bar_price_is_its_open_or_its_worst_for_the_side() {
393 let bar = MarketBar::new(d("100"), d("110"), d("90"), d("105"));
394
395 assert_eq!(NextBarPrice::Open.reference(Side::Buy, &bar), d("100"));
396 assert_eq!(NextBarPrice::Open.reference(Side::Sell, &bar), d("100"));
397 assert_eq!(
398 NextBarPrice::AdverseExtreme.reference(Side::Buy, &bar),
399 d("110")
400 );
401 assert_eq!(
402 NextBarPrice::AdverseExtreme.reference(Side::Sell, &bar),
403 d("90")
404 );
405 }
406
407 #[test]
408 fn market_order_timing_defaults_to_on_arrival_and_round_trips() {
409 assert_eq!(MarketOrderTiming::default(), MarketOrderTiming::OnArrival);
410
411 for timing in [
412 MarketOrderTiming::OnArrival,
413 MarketOrderTiming::NextBar(NextBarPrice::Open),
414 MarketOrderTiming::NextBar(NextBarPrice::AdverseExtreme),
415 ] {
416 let json = serde_json::to_string(&timing).unwrap();
417 assert_eq!(
418 serde_json::from_str::<MarketOrderTiming>(&json).unwrap(),
419 timing
420 );
421 }
422 }
423
424 #[test]
425 fn stale_market_policy_defaults_to_off_and_round_trips() {
426 assert_eq!(StaleMarketPolicy::default(), StaleMarketPolicy::Off);
427
428 let wait = StaleMarketPolicy::Wait {
429 max_age: Duration::from_secs(90),
430 };
431 assert_eq!(
432 serde_json::to_string(&wait).unwrap(),
433 r#"{"Wait":{"max_age":{"secs":90,"nanos":0}}}"#,
434 "the shape the rustdoc promises a configuration file"
435 );
436
437 for policy in [StaleMarketPolicy::Off, wait] {
438 let json = serde_json::to_string(&policy).unwrap();
439 assert_eq!(
440 serde_json::from_str::<StaleMarketPolicy>(&json).unwrap(),
441 policy
442 );
443 }
444 }
445
446 fn d(s: &str) -> Decimal {
447 s.parse().unwrap()
448 }
449
450 /// Bid 99.5 / ask 100.5 / last 100.0 — a complete snapshot, mid also 100.0.
451 fn prices() -> MarketSnapshot {
452 MarketSnapshot::new(Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
453 }
454
455 /// A snapshot from three literal prices, for the partial-book cases.
456 fn snapshot(
457 bid: Option<Decimal>,
458 ask: Option<Decimal>,
459 last: Option<Decimal>,
460 ) -> MarketSnapshot {
461 MarketSnapshot::new(bid, ask, last)
462 }
463
464 #[test]
465 fn last_price_market_buy_uses_last() {
466 let market = prices();
467 assert_eq!(
468 LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
469 Some(d("100.0"))
470 );
471 }
472
473 #[test]
474 fn bid_ask_market_buy_uses_ask() {
475 let market = prices();
476 assert_eq!(
477 BidAskFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
478 Some(d("100.5"))
479 );
480 }
481
482 #[test]
483 fn bid_ask_market_sell_uses_bid() {
484 let market = prices();
485 assert_eq!(
486 BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
487 Some(d("99.5"))
488 );
489 }
490
491 #[test]
492 fn midpoint_uses_mid() {
493 let market = prices();
494 assert_eq!(
495 MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
496 Some(d("100.0"))
497 );
498 }
499
500 #[test]
501 fn midpoint_falls_back_to_last_when_no_bid_ask() {
502 assert_eq!(
503 MidpointFillModel.fill_price(&FillContext::new(
504 Side::Buy,
505 Decimal::ONE,
506 &snapshot(None, None, Some(d("100.0")))
507 )),
508 Some(d("100.0"))
509 );
510 }
511
512 // --- SimFillConfig enum dispatch ---
513
514 #[test]
515 fn fill_model_config_last_price_dispatches() {
516 let market = prices();
517 let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
518 assert_eq!(
519 cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
520 LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
521 );
522 }
523
524 #[test]
525 fn fill_model_config_bid_ask_dispatches() {
526 let market = prices();
527 let cfg = SimFillConfig::BidAsk(BidAskFillModel);
528 assert_eq!(
529 cfg.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
530 BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market))
531 );
532 }
533
534 #[test]
535 fn fill_model_config_midpoint_dispatches() {
536 let market = prices();
537 let cfg = SimFillConfig::Midpoint(MidpointFillModel);
538 assert_eq!(
539 cfg.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
540 MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market))
541 );
542 }
543
544 #[test]
545 fn fill_model_config_default_is_last_price() {
546 assert_eq!(
547 SimFillConfig::default(),
548 SimFillConfig::LastPrice(LastPriceFillModel)
549 );
550 }
551
552 // --- Edge cases ---
553
554 #[test]
555 fn last_price_all_none_returns_none() {
556 // No market data at all — e.g. first tick of a backtest before any prices arrive.
557 // The mock exchange falls back to request.state.price when fill_price returns None.
558 assert_eq!(
559 LastPriceFillModel.fill_price(&FillContext::new(
560 Side::Buy,
561 Decimal::ONE,
562 &snapshot(None, None, None)
563 )),
564 None
565 );
566 assert_eq!(
567 LastPriceFillModel.fill_price(&FillContext::new(
568 Side::Sell,
569 Decimal::ONE,
570 &snapshot(None, None, None)
571 )),
572 None
573 );
574 }
575
576 #[test]
577 fn last_price_falls_back_to_bid_ask_when_no_last_price() {
578 // When last_price=None but bid/ask are present, the model falls back to
579 // bid/ask (as documented in the fallback chain). This exercises the tertiary
580 // fallback that was previously untested.
581 let market = snapshot(Some(d("99.5")), Some(d("100.5")), None);
582 assert_eq!(
583 LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
584 Some(d("100.5")),
585 "Buy with no last_price should fall back to best_ask"
586 );
587 assert_eq!(
588 LastPriceFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &market)),
589 Some(d("99.5")),
590 "Sell with no last_price should fall back to best_bid"
591 );
592 }
593
594 /// A partial book falls back to the last price, whichever side is missing.
595 ///
596 /// These replace a pair that asserted the *limit* beat a stale `last_price` on a partial book.
597 /// That special case existed only to stop a limit buy filling above its own limit, which is now
598 /// the venue's job — and doing it here was what let the complete-book arm keep filling above a
599 /// limit unnoticed.
600 #[test]
601 fn midpoint_with_only_bid_falls_back_to_last() {
602 assert_eq!(
603 MidpointFillModel.fill_price(&FillContext::new(
604 Side::Buy,
605 Decimal::ONE,
606 &snapshot(Some(d("99.5")), None, Some(d("100.0")))
607 )),
608 Some(d("100.0"))
609 );
610 }
611
612 #[test]
613 fn midpoint_with_only_ask_falls_back_to_last() {
614 assert_eq!(
615 MidpointFillModel.fill_price(&FillContext::new(
616 Side::Sell,
617 Decimal::ONE,
618 &snapshot(None, Some(d("100.5")), Some(d("100.0")))
619 )),
620 Some(d("100.0"))
621 );
622 }
623
624 /// A complete book prices at the midpoint even where that sits far from the last print.
625 ///
626 /// This case had no coverage at all, and it is where the model used to fill a limit buy above
627 /// its own limit: `mid_price()` won outright and the limit was never consulted. There is no
628 /// limit to consult now — bounding the result is the venue's job — so what this pins is that
629 /// the midpoint wins over `last_price`, which is the whole content of the model.
630 #[test]
631 fn midpoint_complete_book_prices_at_mid_not_last() {
632 let market = snapshot(Some(d("99.5")), Some(d("100.5")), Some(d("110.0")));
633 assert_eq!(
634 MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &market)),
635 Some(d("100.0")),
636 "the midpoint of 99.5/100.5, not the stale 110.0 print"
637 );
638 }
639
640 /// No model reads the side it is not given, and none invents a price from nothing.
641 #[test]
642 fn every_model_returns_none_on_an_empty_snapshot() {
643 let empty = MarketSnapshot::default();
644 for side in [Side::Buy, Side::Sell] {
645 let fill = FillContext::new(side, Decimal::ONE, &empty);
646 assert_eq!(LastPriceFillModel.fill_price(&fill), None);
647 assert_eq!(BidAskFillModel.fill_price(&fill), None);
648 assert_eq!(MidpointFillModel.fill_price(&fill), None);
649 }
650 }
651}