1use rust_decimal::Decimal;
2use rustrade_instrument::Side;
3use serde::{Deserialize, Serialize};
4
5pub trait FillModel {
35 fn fill_price(
36 &self,
37 side: Side,
38 order_price: Option<Decimal>,
39 best_bid: Option<Decimal>,
40 best_ask: Option<Decimal>,
41 last_price: Option<Decimal>,
42 ) -> Option<Decimal>;
43}
44
45#[derive(
53 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
54)]
55pub struct LastPriceFillModel;
56
57impl FillModel for LastPriceFillModel {
58 fn fill_price(
59 &self,
60 side: Side,
61 order_price: Option<Decimal>,
62 best_bid: Option<Decimal>,
63 best_ask: Option<Decimal>,
64 last_price: Option<Decimal>,
65 ) -> Option<Decimal> {
66 order_price.or(last_price).or(match side {
67 Side::Buy => best_ask,
68 Side::Sell => best_bid,
69 })
70 }
71}
72
73#[derive(
83 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
84)]
85pub struct BidAskFillModel;
86
87impl FillModel for BidAskFillModel {
88 fn fill_price(
89 &self,
90 side: Side,
91 order_price: Option<Decimal>,
92 best_bid: Option<Decimal>,
93 best_ask: Option<Decimal>,
94 last_price: Option<Decimal>,
95 ) -> Option<Decimal> {
96 if let Some(limit) = order_price {
97 return Some(limit);
100 }
101 match side {
103 Side::Buy => best_ask.or(last_price),
104 Side::Sell => best_bid.or(last_price),
105 }
106 }
107}
108
109#[derive(
131 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
132)]
133pub struct MidpointFillModel;
134
135impl FillModel for MidpointFillModel {
136 fn fill_price(
137 &self,
138 _side: Side,
139 order_price: Option<Decimal>,
140 best_bid: Option<Decimal>,
141 best_ask: Option<Decimal>,
142 last_price: Option<Decimal>,
143 ) -> Option<Decimal> {
144 match (best_bid, best_ask) {
145 (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
146 _ => order_price.or(last_price),
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
157pub enum SimFillConfig {
158 LastPrice(LastPriceFillModel),
159 BidAsk(BidAskFillModel),
160 Midpoint(MidpointFillModel),
161}
162
163impl Default for SimFillConfig {
164 fn default() -> Self {
165 Self::LastPrice(LastPriceFillModel)
166 }
167}
168
169impl FillModel for SimFillConfig {
170 fn fill_price(
171 &self,
172 side: Side,
173 order_price: Option<Decimal>,
174 best_bid: Option<Decimal>,
175 best_ask: Option<Decimal>,
176 last_price: Option<Decimal>,
177 ) -> Option<Decimal> {
178 match self {
179 SimFillConfig::LastPrice(m) => {
180 m.fill_price(side, order_price, best_bid, best_ask, last_price)
181 }
182 SimFillConfig::BidAsk(m) => {
183 m.fill_price(side, order_price, best_bid, best_ask, last_price)
184 }
185 SimFillConfig::Midpoint(m) => {
186 m.fill_price(side, order_price, best_bid, best_ask, last_price)
187 }
188 }
189 }
190}
191
192#[cfg(test)]
193#[allow(clippy::unwrap_used)] mod tests {
195 use super::*;
196
197 fn d(s: &str) -> Decimal {
198 s.parse().unwrap()
199 }
200
201 fn prices() -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
202 (Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
203 }
204
205 #[test]
206 fn last_price_market_buy_uses_last() {
207 let (bid, ask, last) = prices();
208 assert_eq!(
209 LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
210 Some(d("100.0"))
211 );
212 }
213
214 #[test]
215 fn last_price_limit_uses_order_price() {
216 let (bid, ask, last) = prices();
217 assert_eq!(
218 LastPriceFillModel.fill_price(Side::Buy, Some(d("99.0")), bid, ask, last),
219 Some(d("99.0"))
220 );
221 }
222
223 #[test]
224 fn bid_ask_market_buy_uses_ask() {
225 let (bid, ask, last) = prices();
226 assert_eq!(
227 BidAskFillModel.fill_price(Side::Buy, None, bid, ask, last),
228 Some(d("100.5"))
229 );
230 }
231
232 #[test]
233 fn bid_ask_market_sell_uses_bid() {
234 let (bid, ask, last) = prices();
235 assert_eq!(
236 BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
237 Some(d("99.5"))
238 );
239 }
240
241 #[test]
242 fn midpoint_uses_mid() {
243 let (bid, ask, last) = prices();
244 assert_eq!(
245 MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
246 Some(d("100.0"))
247 );
248 }
249
250 #[test]
251 fn midpoint_falls_back_to_last_when_no_bid_ask() {
252 assert_eq!(
253 MidpointFillModel.fill_price(Side::Buy, None, None, None, Some(d("100.0"))),
254 Some(d("100.0"))
255 );
256 }
257
258 #[test]
261 fn fill_model_config_last_price_dispatches() {
262 let (bid, ask, last) = prices();
263 let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
264 assert_eq!(
265 cfg.fill_price(Side::Buy, None, bid, ask, last),
266 LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
267 );
268 }
269
270 #[test]
271 fn fill_model_config_bid_ask_dispatches() {
272 let (bid, ask, last) = prices();
273 let cfg = SimFillConfig::BidAsk(BidAskFillModel);
274 assert_eq!(
275 cfg.fill_price(Side::Sell, None, bid, ask, last),
276 BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
277 );
278 }
279
280 #[test]
281 fn fill_model_config_midpoint_dispatches() {
282 let (bid, ask, last) = prices();
283 let cfg = SimFillConfig::Midpoint(MidpointFillModel);
284 assert_eq!(
285 cfg.fill_price(Side::Buy, None, bid, ask, last),
286 MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
287 );
288 }
289
290 #[test]
291 fn fill_model_config_default_is_last_price() {
292 assert_eq!(
293 SimFillConfig::default(),
294 SimFillConfig::LastPrice(LastPriceFillModel)
295 );
296 }
297
298 #[test]
301 fn last_price_all_none_returns_none() {
302 assert_eq!(
305 LastPriceFillModel.fill_price(Side::Buy, None, None, None, None),
306 None
307 );
308 assert_eq!(
309 LastPriceFillModel.fill_price(Side::Sell, None, None, None, None),
310 None
311 );
312 }
313
314 #[test]
315 fn last_price_falls_back_to_bid_ask_when_no_last_price() {
316 assert_eq!(
320 LastPriceFillModel.fill_price(Side::Buy, None, Some(d("99.5")), Some(d("100.5")), None),
321 Some(d("100.5")),
322 "Buy with no last_price should fall back to best_ask"
323 );
324 assert_eq!(
325 LastPriceFillModel.fill_price(
326 Side::Sell,
327 None,
328 Some(d("99.5")),
329 Some(d("100.5")),
330 None
331 ),
332 Some(d("99.5")),
333 "Sell with no last_price should fall back to best_bid"
334 );
335 }
336
337 #[test]
338 fn bid_ask_limit_order_wins_over_bid_ask() {
339 let (bid, ask, last) = prices();
341 let limit = Some(d("98.0"));
342 assert_eq!(
343 BidAskFillModel.fill_price(Side::Buy, limit, bid, ask, last),
344 limit,
345 "limit price should beat best_ask for buy"
346 );
347 assert_eq!(
348 BidAskFillModel.fill_price(Side::Sell, limit, bid, ask, last),
349 limit,
350 "limit price should beat best_bid for sell"
351 );
352 }
353
354 #[test]
355 fn midpoint_with_only_bid_falls_back_to_last() {
356 assert_eq!(
358 MidpointFillModel.fill_price(Side::Buy, None, Some(d("99.5")), None, Some(d("100.0"))),
359 Some(d("100.0"))
360 );
361 }
362
363 #[test]
364 fn midpoint_with_only_ask_falls_back_to_last() {
365 assert_eq!(
368 MidpointFillModel.fill_price(
369 Side::Sell,
370 None,
371 None,
372 Some(d("100.5")),
373 Some(d("100.0"))
374 ),
375 Some(d("100.0"))
376 );
377 }
378
379 #[test]
380 fn midpoint_partial_book_prefers_order_price_over_last() {
381 assert_eq!(
385 MidpointFillModel.fill_price(
386 Side::Buy,
387 Some(d("100.0")),
388 Some(d("99.5")),
389 None,
390 Some(d("110.0"))
391 ),
392 Some(d("100.0")),
393 "partial book: limit price should beat stale last_price"
394 );
395 }
396}