use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
pub trait FillModel {
fn fill_price(
&self,
side: Side,
order_price: Option<Decimal>,
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> 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,
side: Side,
order_price: Option<Decimal>,
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> Option<Decimal> {
order_price.or(last_price).or(match side {
Side::Buy => best_ask,
Side::Sell => 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,
side: Side,
order_price: Option<Decimal>,
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> Option<Decimal> {
if let Some(limit) = order_price {
return Some(limit);
}
match side {
Side::Buy => best_ask.or(last_price),
Side::Sell => best_bid.or(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,
_side: Side,
order_price: Option<Decimal>,
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> Option<Decimal> {
match (best_bid, best_ask) {
(Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
_ => order_price.or(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,
side: Side,
order_price: Option<Decimal>,
best_bid: Option<Decimal>,
best_ask: Option<Decimal>,
last_price: Option<Decimal>,
) -> Option<Decimal> {
match self {
SimFillConfig::LastPrice(m) => {
m.fill_price(side, order_price, best_bid, best_ask, last_price)
}
SimFillConfig::BidAsk(m) => {
m.fill_price(side, order_price, best_bid, best_ask, last_price)
}
SimFillConfig::Midpoint(m) => {
m.fill_price(side, order_price, best_bid, best_ask, last_price)
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn d(s: &str) -> Decimal {
s.parse().unwrap()
}
fn prices() -> (Option<Decimal>, Option<Decimal>, Option<Decimal>) {
(Some(d("99.5")), Some(d("100.5")), Some(d("100.0")))
}
#[test]
fn last_price_market_buy_uses_last() {
let (bid, ask, last) = prices();
assert_eq!(
LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
Some(d("100.0"))
);
}
#[test]
fn last_price_limit_uses_order_price() {
let (bid, ask, last) = prices();
assert_eq!(
LastPriceFillModel.fill_price(Side::Buy, Some(d("99.0")), bid, ask, last),
Some(d("99.0"))
);
}
#[test]
fn bid_ask_market_buy_uses_ask() {
let (bid, ask, last) = prices();
assert_eq!(
BidAskFillModel.fill_price(Side::Buy, None, bid, ask, last),
Some(d("100.5"))
);
}
#[test]
fn bid_ask_market_sell_uses_bid() {
let (bid, ask, last) = prices();
assert_eq!(
BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
Some(d("99.5"))
);
}
#[test]
fn midpoint_uses_mid() {
let (bid, ask, last) = prices();
assert_eq!(
MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
Some(d("100.0"))
);
}
#[test]
fn midpoint_falls_back_to_last_when_no_bid_ask() {
assert_eq!(
MidpointFillModel.fill_price(Side::Buy, None, None, None, Some(d("100.0"))),
Some(d("100.0"))
);
}
#[test]
fn fill_model_config_last_price_dispatches() {
let (bid, ask, last) = prices();
let cfg = SimFillConfig::LastPrice(LastPriceFillModel);
assert_eq!(
cfg.fill_price(Side::Buy, None, bid, ask, last),
LastPriceFillModel.fill_price(Side::Buy, None, bid, ask, last),
);
}
#[test]
fn fill_model_config_bid_ask_dispatches() {
let (bid, ask, last) = prices();
let cfg = SimFillConfig::BidAsk(BidAskFillModel);
assert_eq!(
cfg.fill_price(Side::Sell, None, bid, ask, last),
BidAskFillModel.fill_price(Side::Sell, None, bid, ask, last),
);
}
#[test]
fn fill_model_config_midpoint_dispatches() {
let (bid, ask, last) = prices();
let cfg = SimFillConfig::Midpoint(MidpointFillModel);
assert_eq!(
cfg.fill_price(Side::Buy, None, bid, ask, last),
MidpointFillModel.fill_price(Side::Buy, None, bid, ask, last),
);
}
#[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(Side::Buy, None, None, None, None),
None
);
assert_eq!(
LastPriceFillModel.fill_price(Side::Sell, None, None, None, None),
None
);
}
#[test]
fn last_price_falls_back_to_bid_ask_when_no_last_price() {
assert_eq!(
LastPriceFillModel.fill_price(Side::Buy, None, Some(d("99.5")), Some(d("100.5")), None),
Some(d("100.5")),
"Buy with no last_price should fall back to best_ask"
);
assert_eq!(
LastPriceFillModel.fill_price(
Side::Sell,
None,
Some(d("99.5")),
Some(d("100.5")),
None
),
Some(d("99.5")),
"Sell with no last_price should fall back to best_bid"
);
}
#[test]
fn bid_ask_limit_order_wins_over_bid_ask() {
let (bid, ask, last) = prices();
let limit = Some(d("98.0"));
assert_eq!(
BidAskFillModel.fill_price(Side::Buy, limit, bid, ask, last),
limit,
"limit price should beat best_ask for buy"
);
assert_eq!(
BidAskFillModel.fill_price(Side::Sell, limit, bid, ask, last),
limit,
"limit price should beat best_bid for sell"
);
}
#[test]
fn midpoint_with_only_bid_falls_back_to_last() {
assert_eq!(
MidpointFillModel.fill_price(Side::Buy, None, 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(
Side::Sell,
None,
None,
Some(d("100.5")),
Some(d("100.0"))
),
Some(d("100.0"))
);
}
#[test]
fn midpoint_partial_book_prefers_order_price_over_last() {
assert_eq!(
MidpointFillModel.fill_price(
Side::Buy,
Some(d("100.0")),
Some(d("99.5")),
None,
Some(d("110.0"))
),
Some(d("100.0")),
"partial book: limit price should beat stale last_price"
);
}
}