1use rust_decimal::Decimal;
2use rustrade_instrument::Side;
3use serde::{Deserialize, Serialize};
4
5pub trait FillModel {
27 fn fill_price(
28 &self,
29 side: Side,
30 order_price: Option<Decimal>,
31 best_bid: Option<Decimal>,
32 best_ask: Option<Decimal>,
33 last_price: Option<Decimal>,
34 ) -> Option<Decimal>;
35}
36
37#[derive(
46 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
47)]
48pub struct LastPriceFillModel;
49
50impl FillModel for LastPriceFillModel {
51 fn fill_price(
52 &self,
53 side: Side,
54 order_price: Option<Decimal>,
55 best_bid: Option<Decimal>,
56 best_ask: Option<Decimal>,
57 last_price: Option<Decimal>,
58 ) -> Option<Decimal> {
59 order_price.or(last_price).or(match side {
60 Side::Buy => best_ask,
61 Side::Sell => best_bid,
62 })
63 }
64}
65
66#[derive(
76 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
77)]
78pub struct BidAskFillModel;
79
80impl FillModel for BidAskFillModel {
81 fn fill_price(
82 &self,
83 side: Side,
84 order_price: Option<Decimal>,
85 best_bid: Option<Decimal>,
86 best_ask: Option<Decimal>,
87 last_price: Option<Decimal>,
88 ) -> Option<Decimal> {
89 if let Some(limit) = order_price {
90 return Some(limit);
93 }
94 match side {
96 Side::Buy => best_ask.or(last_price),
97 Side::Sell => best_bid.or(last_price),
98 }
99 }
100}
101
102#[derive(
124 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
125)]
126pub struct MidpointFillModel;
127
128impl FillModel for MidpointFillModel {
129 fn fill_price(
130 &self,
131 _side: Side,
132 order_price: Option<Decimal>,
133 best_bid: Option<Decimal>,
134 best_ask: Option<Decimal>,
135 last_price: Option<Decimal>,
136 ) -> Option<Decimal> {
137 match (best_bid, best_ask) {
138 (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
139 _ => order_price.or(last_price),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
150pub enum SimFillConfig {
151 LastPrice(LastPriceFillModel),
152 BidAsk(BidAskFillModel),
153 Midpoint(MidpointFillModel),
154}
155
156impl Default for SimFillConfig {
157 fn default() -> Self {
158 Self::LastPrice(LastPriceFillModel)
159 }
160}
161
162impl FillModel for SimFillConfig {
163 fn fill_price(
164 &self,
165 side: Side,
166 order_price: Option<Decimal>,
167 best_bid: Option<Decimal>,
168 best_ask: Option<Decimal>,
169 last_price: Option<Decimal>,
170 ) -> Option<Decimal> {
171 match self {
172 SimFillConfig::LastPrice(m) => {
173 m.fill_price(side, order_price, best_bid, best_ask, last_price)
174 }
175 SimFillConfig::BidAsk(m) => {
176 m.fill_price(side, order_price, best_bid, best_ask, last_price)
177 }
178 SimFillConfig::Midpoint(m) => {
179 m.fill_price(side, order_price, best_bid, best_ask, last_price)
180 }
181 }
182 }
183}
184
185#[cfg(test)]
186#[allow(clippy::unwrap_used)] mod tests {
188 use super::*;
189
190 fn d(s: &str) -> Decimal {
191 s.parse().unwrap()
192 }
193
194 fn prices() -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
195 (Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
196 }
197
198 #[test]
199 fn last_price_market_buy_uses_last() {
200 let (bid, ask, last) = prices();
201 assert_eq!(
202 LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
203 Some(d("100.0"))
204 );
205 }
206
207 #[test]
208 fn last_price_limit_uses_order_price() {
209 let (bid, ask, last) = prices();
210 assert_eq!(
211 LastPriceFillModel.fill_price(Side::Buy, Some(d("99.0")), bid, ask, last),
212 Some(d("99.0"))
213 );
214 }
215
216 #[test]
217 fn bid_ask_market_buy_uses_ask() {
218 let (bid, ask, last) = prices();
219 assert_eq!(
220 BidAskFillModel.fill_price(Side::Buy, None, bid, ask, last),
221 Some(d("100.5"))
222 );
223 }
224
225 #[test]
226 fn bid_ask_market_sell_uses_bid() {
227 let (bid, ask, last) = prices();
228 assert_eq!(
229 BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
230 Some(d("99.5"))
231 );
232 }
233
234 #[test]
235 fn midpoint_uses_mid() {
236 let (bid, ask, last) = prices();
237 assert_eq!(
238 MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
239 Some(d("100.0"))
240 );
241 }
242
243 #[test]
244 fn midpoint_falls_back_to_last_when_no_bid_ask() {
245 assert_eq!(
246 MidpointFillModel.fill_price(Side::Buy, None, None, None, Some(d("100.0"))),
247 Some(d("100.0"))
248 );
249 }
250
251 #[test]
254 fn fill_model_config_last_price_dispatches() {
255 let (bid, ask, last) = prices();
256 let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
257 assert_eq!(
258 cfg.fill_price(Side::Buy, None, bid, ask, last),
259 LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
260 );
261 }
262
263 #[test]
264 fn fill_model_config_bid_ask_dispatches() {
265 let (bid, ask, last) = prices();
266 let cfg = SimFillConfig::BidAsk(BidAskFillModel);
267 assert_eq!(
268 cfg.fill_price(Side::Sell, None, bid, ask, last),
269 BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
270 );
271 }
272
273 #[test]
274 fn fill_model_config_midpoint_dispatches() {
275 let (bid, ask, last) = prices();
276 let cfg = SimFillConfig::Midpoint(MidpointFillModel);
277 assert_eq!(
278 cfg.fill_price(Side::Buy, None, bid, ask, last),
279 MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
280 );
281 }
282
283 #[test]
284 fn fill_model_config_default_is_last_price() {
285 assert_eq!(
286 SimFillConfig::default(),
287 SimFillConfig::LastPrice(LastPriceFillModel)
288 );
289 }
290
291 #[test]
294 fn last_price_all_none_returns_none() {
295 assert_eq!(
298 LastPriceFillModel.fill_price(Side::Buy, None, None, None, None),
299 None
300 );
301 assert_eq!(
302 LastPriceFillModel.fill_price(Side::Sell, None, None, None, None),
303 None
304 );
305 }
306
307 #[test]
308 fn last_price_falls_back_to_bid_ask_when_no_last_price() {
309 assert_eq!(
313 LastPriceFillModel.fill_price(Side::Buy, None, Some(d("99.5")), Some(d("100.5")), None),
314 Some(d("100.5")),
315 "Buy with no last_price should fall back to best_ask"
316 );
317 assert_eq!(
318 LastPriceFillModel.fill_price(
319 Side::Sell,
320 None,
321 Some(d("99.5")),
322 Some(d("100.5")),
323 None
324 ),
325 Some(d("99.5")),
326 "Sell with no last_price should fall back to best_bid"
327 );
328 }
329
330 #[test]
331 fn bid_ask_limit_order_wins_over_bid_ask() {
332 let (bid, ask, last) = prices();
334 let limit = Some(d("98.0"));
335 assert_eq!(
336 BidAskFillModel.fill_price(Side::Buy, limit, bid, ask, last),
337 limit,
338 "limit price should beat best_ask for buy"
339 );
340 assert_eq!(
341 BidAskFillModel.fill_price(Side::Sell, limit, bid, ask, last),
342 limit,
343 "limit price should beat best_bid for sell"
344 );
345 }
346
347 #[test]
348 fn midpoint_with_only_bid_falls_back_to_last() {
349 assert_eq!(
351 MidpointFillModel.fill_price(Side::Buy, None, Some(d("99.5")), None, Some(d("100.0"))),
352 Some(d("100.0"))
353 );
354 }
355
356 #[test]
357 fn midpoint_with_only_ask_falls_back_to_last() {
358 assert_eq!(
361 MidpointFillModel.fill_price(
362 Side::Sell,
363 None,
364 None,
365 Some(d("100.5")),
366 Some(d("100.0"))
367 ),
368 Some(d("100.0"))
369 );
370 }
371
372 #[test]
373 fn midpoint_partial_book_prefers_order_price_over_last() {
374 assert_eq!(
378 MidpointFillModel.fill_price(
379 Side::Buy,
380 Some(d("100.0")),
381 Some(d("99.5")),
382 None,
383 Some(d("110.0"))
384 ),
385 Some(d("100.0")),
386 "partial book: limit price should beat stale last_price"
387 );
388 }
389}