use crate::market::MarketSnapshot;
use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct FillContext<'a> {
pub side: Side,
pub market: &'a MarketSnapshot,
}
impl<'a> FillContext<'a> {
pub fn new(side: Side, market: &'a MarketSnapshot) -> Self {
Self { side, market }
}
}
pub trait FillModel {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal>;
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
pub struct LastPriceFillModel;
impl FillModel for LastPriceFillModel {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
fill.market.last_price.or(match fill.side {
Side::Buy => fill.market.best_ask,
Side::Sell => fill.market.best_bid,
})
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
pub struct BidAskFillModel;
impl FillModel for BidAskFillModel {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
match fill.side {
Side::Buy => fill.market.best_ask.or(fill.market.last_price),
Side::Sell => fill.market.best_bid.or(fill.market.last_price),
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
pub struct MidpointFillModel;
impl FillModel for MidpointFillModel {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
fill.market.mid_price().or(fill.market.last_price)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub enum SimFillConfig {
LastPrice(LastPriceFillModel),
BidAsk(BidAskFillModel),
Midpoint(MidpointFillModel),
}
impl Default for SimFillConfig {
fn default() -> Self {
Self::LastPrice(LastPriceFillModel)
}
}
impl FillModel for SimFillConfig {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
match self {
SimFillConfig::LastPrice(model) => model.fill_price(fill),
SimFillConfig::BidAsk(model) => model.fill_price(fill),
SimFillConfig::Midpoint(model) => model.fill_price(fill),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn d(s: &str) -> Decimal {
s.parse().unwrap()
}
fn prices() -> MarketSnapshot {
MarketSnapshot::new(Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
}
fn snapshot(
bid: Option<Decimal>,
ask: Option<Decimal>,
last: Option<Decimal>,
) -> MarketSnapshot {
MarketSnapshot::new(bid, ask, last)
}
#[test]
fn last_price_market_buy_uses_last() {
let market = prices();
assert_eq!(
LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, &market)),
Some(d("100.0"))
);
}
#[test]
fn bid_ask_market_buy_uses_ask() {
let market = prices();
assert_eq!(
BidAskFillModel.fill_price(&FillContext::new(Side::Buy, &market)),
Some(d("100.5"))
);
}
#[test]
fn bid_ask_market_sell_uses_bid() {
let market = prices();
assert_eq!(
BidAskFillModel.fill_price(&FillContext::new(Side::Sell, &market)),
Some(d("99.5"))
);
}
#[test]
fn midpoint_uses_mid() {
let market = prices();
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(Side::Buy, &market)),
Some(d("100.0"))
);
}
#[test]
fn midpoint_falls_back_to_last_when_no_bid_ask() {
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(
Side::Buy,
&snapshot(None, None, Some(d("100.0")))
)),
Some(d("100.0"))
);
}
#[test]
fn fill_model_config_last_price_dispatches() {
let market = prices();
let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
assert_eq!(
cfg.fill_price(&FillContext::new(Side::Buy, &market)),
LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, &market))
);
}
#[test]
fn fill_model_config_bid_ask_dispatches() {
let market = prices();
let cfg = SimFillConfig::BidAsk(BidAskFillModel);
assert_eq!(
cfg.fill_price(&FillContext::new(Side::Sell, &market)),
BidAskFillModel.fill_price(&FillContext::new(Side::Sell, &market))
);
}
#[test]
fn fill_model_config_midpoint_dispatches() {
let market = prices();
let cfg = SimFillConfig::Midpoint(MidpointFillModel);
assert_eq!(
cfg.fill_price(&FillContext::new(Side::Buy, &market)),
MidpointFillModel.fill_price(&FillContext::new(Side::Buy, &market))
);
}
#[test]
fn fill_model_config_default_is_last_price() {
assert_eq!(
SimFillConfig::default(),
SimFillConfig::LastPrice(LastPriceFillModel)
);
}
#[test]
fn last_price_all_none_returns_none() {
assert_eq!(
LastPriceFillModel
.fill_price(&FillContext::new(Side::Buy, &snapshot(None, None, None))),
None
);
assert_eq!(
LastPriceFillModel
.fill_price(&FillContext::new(Side::Sell, &snapshot(None, None, None))),
None
);
}
#[test]
fn last_price_falls_back_to_bid_ask_when_no_last_price() {
let market = snapshot(Some(d("99.5")), Some(d("100.5")), None);
assert_eq!(
LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, &market)),
Some(d("100.5")),
"Buy with no last_price should fall back to best_ask"
);
assert_eq!(
LastPriceFillModel.fill_price(&FillContext::new(Side::Sell, &market)),
Some(d("99.5")),
"Sell with no last_price should fall back to best_bid"
);
}
#[test]
fn midpoint_with_only_bid_falls_back_to_last() {
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(
Side::Buy,
&snapshot(Some(d("99.5")), None, Some(d("100.0")))
)),
Some(d("100.0"))
);
}
#[test]
fn midpoint_with_only_ask_falls_back_to_last() {
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(
Side::Sell,
&snapshot(None, Some(d("100.5")), Some(d("100.0")))
)),
Some(d("100.0"))
);
}
#[test]
fn midpoint_complete_book_prices_at_mid_not_last() {
let market = snapshot(Some(d("99.5")), Some(d("100.5")), Some(d("110.0")));
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(Side::Buy, &market)),
Some(d("100.0")),
"the midpoint of 99.5/100.5, not the stale 110.0 print"
);
}
#[test]
fn every_model_returns_none_on_an_empty_snapshot() {
let empty = MarketSnapshot::default();
for side in [Side::Buy, Side::Sell] {
let fill = FillContext::new(side, &empty);
assert_eq!(LastPriceFillModel.fill_price(&fill), None);
assert_eq!(BidAskFillModel.fill_price(&fill), None);
assert_eq!(MidpointFillModel.fill_price(&fill), None);
}
}
}