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/// How much is on offer at each side's best price, where the feed says.
87///
88/// Carried **alongside** a [`MarketSnapshot`] rather than inside it. A snapshot is the view a
89/// [`FillModel`](crate::fill::FillModel) prices from and the view
90/// [`RequestOpen::market`](crate::order::request::RequestOpen::market) persists, and a size is
91/// neither: a fill model answers *where* a taker prints, while *how much* that price can absorb is
92/// the venue's own arithmetic — and only the venue tracks what is left of it as its orders eat
93/// into it.
94///
95/// # Absent size means unlimited, not unfillable
96///
97/// `None` says this feed supplies no size information. That is the ordinary case, not a
98/// degenerate one: a trades-only feed, a candle feed, a bulk price export and a venue with no
99/// market feed at all every one of them report it on every observation. A venue reading `None`
100/// therefore caps nothing. The opposite reading — no size information means no size — would
101/// silently stop every price-only backtest from filling anything.
102///
103/// `Some(amount)` is a size that was actually reported, and `Some(Decimal::ZERO)` is a reported
104/// *absence* of size: there is nothing to take, so an aggressor takes nothing. Whoever derives
105/// this from a feed owns that distinction — a feed publishing prices with a zero amount on every
106/// row is stating the former, not the latter, and must say `None`.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub struct MarketDepth {
109 /// Size resting at [`MarketSnapshot::best_bid`], if the feed supplies one.
110 pub best_bid: Option<Decimal>,
111
112 /// Size resting at [`MarketSnapshot::best_ask`], if the feed supplies one.
113 pub best_ask: Option<Decimal>,
114}
115
116impl MarketDepth {
117 /// Neither side's size is known, so nothing consulting this is capped.
118 ///
119 /// What a feed carrying no book reports, and what every [`MarketDepth::default()`] is.
120 pub const UNKNOWN: Self = Self {
121 best_bid: None,
122 best_ask: None,
123 };
124
125 /// Construct a `MarketDepth` from the size on each side.
126 pub fn new(best_bid: Option<Decimal>, best_ask: Option<Decimal>) -> Self {
127 Self { best_bid, best_ask }
128 }
129
130 /// What an aggressor on `side` can take: the ask for a buy, the bid for a sell.
131 ///
132 /// The far side of the book, because that is the side an order on this one trades against —
133 /// the same rule that decides whether a limit order crosses at all.
134 pub fn available_to(&self, side: Side) -> Option<Decimal> {
135 match side {
136 Side::Buy => self.best_ask,
137 Side::Sell => self.best_bid,
138 }
139 }
140
141 /// Reduces what an aggressor on `side` can still take by `quantity`, never below zero.
142 ///
143 /// A side whose size is unknown stays unknown: there is nothing to draw down, and an
144 /// unbounded quantity less a finite one is still unbounded.
145 pub fn consume(&mut self, side: Side, quantity: Decimal) {
146 debug_assert!(
147 quantity >= Decimal::ZERO,
148 "consuming a negative quantity {quantity} of depth would create size that was never \
149 on offer"
150 );
151
152 let available = match side {
153 Side::Buy => &mut self.best_ask,
154 Side::Sell => &mut self.best_bid,
155 };
156
157 if let Some(available) = available {
158 *available = (*available - quantity).max(Decimal::ZERO);
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use rust_decimal_macros::dec;
167
168 #[test]
169 fn default_snapshot_is_empty() {
170 assert!(MarketSnapshot::default().is_empty());
171 }
172
173 #[test]
174 fn a_snapshot_carrying_any_price_is_not_empty() {
175 assert!(!MarketSnapshot::from_last_price(Some(dec!(100))).is_empty());
176 assert!(!MarketSnapshot::new(Some(dec!(99)), None, None).is_empty());
177 assert!(!MarketSnapshot::new(None, Some(dec!(101)), None).is_empty());
178 }
179
180 #[test]
181 fn depth_is_taken_from_the_far_side_of_the_book() {
182 let depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
183 assert_eq!(depth.available_to(Side::Buy), Some(dec!(3)));
184 assert_eq!(depth.available_to(Side::Sell), Some(dec!(5)));
185 }
186
187 #[test]
188 fn an_unknown_size_stays_unknown_however_much_is_taken() {
189 let mut depth = MarketDepth::UNKNOWN;
190 depth.consume(Side::Buy, dec!(1000));
191 assert_eq!(depth.available_to(Side::Buy), None);
192 assert_eq!(MarketDepth::default(), MarketDepth::UNKNOWN);
193 }
194
195 #[test]
196 fn consuming_draws_down_only_the_side_taken_from() {
197 let mut depth = MarketDepth::new(Some(dec!(5)), Some(dec!(3)));
198 depth.consume(Side::Buy, dec!(2));
199 assert_eq!(depth, MarketDepth::new(Some(dec!(5)), Some(dec!(1))));
200 }
201
202 #[test]
203 fn consuming_more_than_is_on_offer_leaves_nothing_rather_than_a_debt() {
204 let mut depth = MarketDepth::new(None, Some(dec!(3)));
205 depth.consume(Side::Buy, dec!(10));
206 assert_eq!(depth.available_to(Side::Buy), Some(Decimal::ZERO));
207 }
208
209 #[test]
210 fn mid_price_requires_both_sides() {
211 assert_eq!(
212 MarketSnapshot::new(Some(dec!(99)), Some(dec!(101)), None).mid_price(),
213 Some(dec!(100))
214 );
215 assert_eq!(
216 MarketSnapshot::new(Some(dec!(99)), None, None).mid_price(),
217 None
218 );
219 assert_eq!(
220 MarketSnapshot::new(None, Some(dec!(101)), None).mid_price(),
221 None
222 );
223 }
224}