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}