Skip to main content

rustrade_execution/
market.rs

1use rust_decimal::Decimal;
2use rustrade_instrument::Side;
3use serde::{Deserialize, Serialize};
4
5/// Market state for a single instrument at a single instant.
6///
7/// Two things consume this type, and they meet at the simulated venue:
8///
9/// - [`RequestOpen::market`](crate::order::request::RequestOpen::market) carries it as the market
10///   its sender observed at the moment it decided to open the order.
11/// - [`FillModel::fill_price`](crate::fill::FillModel::fill_price) prices a fill from it.
12///
13/// # Every field is optional, and absence is ordinary
14///
15/// A feed supplies what it has. An instrument subscribed to trades alone has a `last_price` and no
16/// book; one subscribed to an L1 book has both sides and no trade price until the first print; one
17/// that has received nothing yet has neither, which is what the first ticks of any backtest look
18/// like. Each [`FillModel`](crate::fill::FillModel) implementation documents which fields it reads
19/// and what it falls back to, and returns `None` when it cannot price the order at all — see
20/// [`MockExchange`](crate::exchange::mock::MockExchange), which turns that `None` into a rejection
21/// naming the instrument rather than a panic.
22///
23/// # This is a snapshot, not a book
24///
25/// It holds best bid and best ask prices only — no sizes, no depth. A fill model can therefore
26/// model the spread but not market impact or partial fills against resting size. Modelling those
27/// needs the venue to own a real book, which is a larger change than this type.
28#[derive(
29    Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize,
30)]
31pub struct MarketSnapshot {
32    /// Best bid price, if the instrument's feed supplies a book.
33    pub best_bid: Option<Decimal>,
34
35    /// Best ask price, if the instrument's feed supplies a book.
36    pub best_ask: Option<Decimal>,
37
38    /// Most recent trade price, if one has been seen.
39    pub last_price: Option<Decimal>,
40}
41
42impl MarketSnapshot {
43    /// Construct a `MarketSnapshot` from its three optional prices.
44    pub fn new(
45        best_bid: Option<Decimal>,
46        best_ask: Option<Decimal>,
47        last_price: Option<Decimal>,
48    ) -> Self {
49        Self {
50            best_bid,
51            best_ask,
52            last_price,
53        }
54    }
55
56    /// Construct a `MarketSnapshot` carrying only a last traded price.
57    ///
58    /// This is what a price-only instrument state can honestly report: a price, and no claim about
59    /// either side of a book.
60    pub fn from_last_price(last_price: Option<Decimal>) -> Self {
61        Self {
62            best_bid: None,
63            best_ask: None,
64            last_price,
65        }
66    }
67
68    /// Whether this snapshot carries no price at all.
69    ///
70    /// True for an instrument that has received no market data yet — distinct from no snapshot
71    /// having been taken, which [`RequestOpen::market`](crate::order::request::RequestOpen::market)
72    /// represents as `None`.
73    pub fn is_empty(&self) -> bool {
74        self.best_bid.is_none() && self.best_ask.is_none() && self.last_price.is_none()
75    }
76
77    /// Mid-price, when both sides of the book are present.
78    pub fn mid_price(&self) -> Option<Decimal> {
79        match (self.best_bid, self.best_ask) {
80            (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
81            _ => None,
82        }
83    }
84}
85
86/// The prices of one completed bar (candle): what traded first, highest, lowest and last over its
87/// period.
88///
89/// Carried **alongside** a [`MarketSnapshot`], like [`MarketDepth`], and only by a feed that has
90/// bars. A snapshot says where the market is now. A bar also says where it went over the period
91/// it covers, which is what a venue holding a market order for the next bar prices it from — see
92/// [`MarketOrderTiming::NextBar`](crate::fill::MarketOrderTiming::NextBar).
93///
94/// No volume and no period: a venue does not need either to price a fill, and a bar's instant is
95/// the observation's own, given beside it. The four prices are taken as given: nothing checks that
96/// `low` and `high` bound the other two.
97///
98/// No `Default`: a bar of four zero prices is not a bar any market printed.
99#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
100pub struct MarketBar {
101    /// The first price of the bar's period.
102    pub open: Decimal,
103    /// The highest price of the bar's period.
104    pub high: Decimal,
105    /// The lowest price of the bar's period.
106    pub low: Decimal,
107    /// The last price of the bar's period.
108    pub close: Decimal,
109}
110
111impl MarketBar {
112    /// Construct a `MarketBar` from its four prices.
113    pub fn new(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> Self {
114        Self {
115            open,
116            high,
117            low,
118            close,
119        }
120    }
121}
122
123/// One observation of one instrument's market, as a driver feeds it to a simulated venue through
124/// [`SimulatedVenue::apply_market`](crate::exchange::mock::SimulatedVenue::apply_market).
125///
126/// Only [`snapshot`](Self::snapshot) is required. Everything else describes what the feed knows
127/// beyond it, and defaults to "nothing more": no size, no bar, and a live observation. Built
128/// with [`new`](Self::new) and the `with_` methods.
129///
130/// `#[non_exhaustive]`: what a feed can tell a venue grows, and each addition arrives as a new
131/// field with a default rather than as a new argument every driver must pass.
132#[non_exhaustive]
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct MarketObservation {
135    /// The market as it now stands. See [`MarketSnapshot`].
136    pub snapshot: MarketSnapshot,
137
138    /// How much is on offer at each side of [`snapshot`](Self::snapshot). See [`MarketDepth`].
139    pub depth: MarketDepth,
140
141    /// The completed bar this observation carries, from a feed that has bars, and `None` from
142    /// every other observation. See [`MarketBar`].
143    pub bar: Option<MarketBar>,
144
145    /// Whether this observation shows the market trading or quoting: a trade, a quote, or a bar
146    /// something traded in.
147    ///
148    /// `false` for an observation that only carries a price forward: a bar reported with zero
149    /// volume, as a feed that fills gaps produces, or an event that is not a price at all. Such an
150    /// observation still moves the price the venue holds, but it is not evidence that anyone could
151    /// trade at it, which is what a venue's
152    /// [`StaleMarketPolicy`](crate::fill::StaleMarketPolicy) measures a market's age from.
153    pub fresh: bool,
154}
155
156impl MarketObservation {
157    /// A fresh observation of `snapshot`, with no size information and no bar.
158    pub fn new(snapshot: MarketSnapshot) -> Self {
159        Self {
160            snapshot,
161            depth: MarketDepth::UNKNOWN,
162            bar: None,
163            fresh: true,
164        }
165    }
166
167    /// This observation, reporting `depth` as the size on offer.
168    #[must_use]
169    pub fn with_depth(self, depth: MarketDepth) -> Self {
170        Self { depth, ..self }
171    }
172
173    /// This observation, carrying `bar`, or no bar for `None`.
174    #[must_use]
175    pub fn with_bar(self, bar: Option<MarketBar>) -> Self {
176        Self { bar, ..self }
177    }
178
179    /// This observation, fresh or not. See [`fresh`](Self::fresh).
180    #[must_use]
181    pub fn with_fresh(self, fresh: bool) -> Self {
182        Self { fresh, ..self }
183    }
184}
185
186/// How much is on offer at each side's best price, where the feed says.
187///
188/// Carried **alongside** a [`MarketSnapshot`] rather than inside it. A snapshot is the view a
189/// [`FillModel`](crate::fill::FillModel) prices from and the view
190/// [`RequestOpen::market`](crate::order::request::RequestOpen::market) persists, and a size is
191/// neither: a fill model answers *where* a taker prints, while *how much* that price can absorb is
192/// the venue's own arithmetic — and only the venue tracks what is left of it as its orders eat
193/// into it.
194///
195/// # Absent size means unlimited, not unfillable
196///
197/// `None` says this feed supplies no size information. That is the ordinary case, not a
198/// degenerate one: a trades-only feed, a candle feed, a bulk price export and a venue with no
199/// market feed at all every one of them report it on every observation. A venue reading `None`
200/// therefore caps nothing. The opposite reading — no size information means no size — would
201/// silently stop every price-only backtest from filling anything.
202///
203/// `Some(amount)` is a size that was actually reported, and `Some(Decimal::ZERO)` is a reported
204/// *absence* of size: there is nothing to take, so an aggressor takes nothing. Whoever derives
205/// this from a feed owns that distinction — a feed publishing prices with a zero amount on every
206/// row is stating the former, not the latter, and must say `None`.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
208pub struct MarketDepth {
209    /// Size resting at [`MarketSnapshot::best_bid`], if the feed supplies one.
210    pub best_bid: Option<Decimal>,
211
212    /// Size resting at [`MarketSnapshot::best_ask`], if the feed supplies one.
213    pub best_ask: Option<Decimal>,
214}
215
216impl MarketDepth {
217    /// Neither side's size is known, so nothing consulting this is capped.
218    ///
219    /// What a feed carrying no book reports, and what every [`MarketDepth::default()`] is.
220    pub const UNKNOWN: Self = Self {
221        best_bid: None,
222        best_ask: None,
223    };
224
225    /// Construct a `MarketDepth` from the size on each side.
226    pub fn new(best_bid: Option<Decimal>, best_ask: Option<Decimal>) -> Self {
227        Self { best_bid, best_ask }
228    }
229
230    /// What an aggressor on `side` can take: the ask for a buy, the bid for a sell.
231    ///
232    /// The far side of the book, because that is the side an order on this one trades against —
233    /// the same rule that decides whether a limit order crosses at all.
234    pub fn available_to(&self, side: Side) -> Option<Decimal> {
235        match side {
236            Side::Buy => self.best_ask,
237            Side::Sell => self.best_bid,
238        }
239    }
240
241    /// Reduces what an aggressor on `side` can still take by `quantity`, never below zero.
242    ///
243    /// A side whose size is unknown stays unknown: there is nothing to draw down, and an
244    /// unbounded quantity less a finite one is still unbounded.
245    pub fn consume(&mut self, side: Side, quantity: Decimal) {
246        debug_assert!(
247            quantity >= Decimal::ZERO,
248            "consuming a negative quantity {quantity} of depth would create size that was never \
249             on offer"
250        );
251
252        let available = match side {
253            Side::Buy => &mut self.best_ask,
254            Side::Sell => &mut self.best_bid,
255        };
256
257        if let Some(available) = available {
258            *available = (*available - quantity).max(Decimal::ZERO);
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use rust_decimal_macros::dec;
267
268    #[test]
269    fn default_snapshot_is_empty() {
270        assert!(MarketSnapshot::default().is_empty());
271    }
272
273    #[test]
274    fn a_snapshot_carrying_any_price_is_not_empty() {
275        assert!(!MarketSnapshot::from_last_price(Some(dec!(100))).is_empty());
276        assert!(!MarketSnapshot::new(Some(dec!(99)), None, None).is_empty());
277        assert!(!MarketSnapshot::new(None, Some(dec!(101)), None).is_empty());
278    }
279
280    #[test]
281    fn an_observation_is_fresh_with_no_size_and_no_bar_until_told_otherwise() {
282        let snapshot = MarketSnapshot::from_last_price(Some(dec!(100)));
283        let observation = MarketObservation::new(snapshot);
284        assert_eq!(
285            (
286                observation.snapshot,
287                observation.depth,
288                observation.bar,
289                observation.fresh
290            ),
291            (snapshot, MarketDepth::UNKNOWN, None, true)
292        );
293
294        let depth = MarketDepth::new(Some(dec!(1)), Some(dec!(2)));
295        let bar = MarketBar::new(dec!(99), dec!(101), dec!(98), dec!(100));
296        let observation = observation
297            .with_depth(depth)
298            .with_bar(Some(bar))
299            .with_fresh(false);
300        assert_eq!(
301            (
302                observation.snapshot,
303                observation.depth,
304                observation.bar,
305                observation.fresh
306            ),
307            (snapshot, depth, Some(bar), false)
308        );
309    }
310
311    #[test]
312    fn depth_is_taken_from_the_far_side_of_the_book() {
313        let depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
314        assert_eq!(depth.available_to(Side::Buy), Some(dec!(3)));
315        assert_eq!(depth.available_to(Side::Sell), Some(dec!(5)));
316    }
317
318    #[test]
319    fn an_unknown_size_stays_unknown_however_much_is_taken() {
320        let mut depth = MarketDepth::UNKNOWN;
321        depth.consume(Side::Buy, dec!(1000));
322        assert_eq!(depth.available_to(Side::Buy), None);
323        assert_eq!(MarketDepth::default(), MarketDepth::UNKNOWN);
324    }
325
326    #[test]
327    fn consuming_draws_down_only_the_side_taken_from() {
328        let mut depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
329        depth.consume(Side::Buy, dec!(2));
330        assert_eq!(depth, MarketDepth::new(Some(dec!(5)), Some(dec!(1))));
331    }
332
333    #[test]
334    fn consuming_more_than_is_on_offer_leaves_nothing_rather_than_a_debt() {
335        let mut depth = MarketDepth::new(None, Some(dec!(3)));
336        depth.consume(Side::Buy, dec!(10));
337        assert_eq!(depth.available_to(Side::Buy), Some(Decimal::ZERO));
338    }
339
340    #[test]
341    fn mid_price_requires_both_sides() {
342        assert_eq!(
343            MarketSnapshot::new(Some(dec!(99)), Some(dec!(101)), None).mid_price(),
344            Some(dec!(100))
345        );
346        assert_eq!(
347            MarketSnapshot::new(Some(dec!(99)), None, None).mid_price(),
348            None
349        );
350        assert_eq!(
351            MarketSnapshot::new(None, Some(dec!(101)), None).mid_price(),
352            None
353        );
354    }
355}