use crate::market::{MarketBar, MarketSnapshot};
use rust_decimal::Decimal;
use rustrade_instrument::Side;
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, time::Duration};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct FillContext<'a> {
pub side: Side,
pub quantity: Decimal,
pub market: &'a MarketSnapshot,
}
impl<'a> FillContext<'a> {
pub fn new(side: Side, quantity: Decimal, market: &'a MarketSnapshot) -> Self {
Self {
side,
quantity,
market,
}
}
}
pub trait FillModel: Debug + Send + Sync {
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),
}
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
)]
#[non_exhaustive]
pub enum MarketOrderTiming {
#[default]
OnArrival,
NextBar(NextBarPrice),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[non_exhaustive]
pub enum NextBarPrice {
Open,
AdverseExtreme,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
)]
#[non_exhaustive]
pub enum StaleMarketPolicy {
#[default]
Off,
Wait {
max_age: Duration,
},
}
impl NextBarPrice {
pub fn reference(self, side: Side, bar: &MarketBar) -> Decimal {
match (self, side) {
(Self::Open, _) => bar.open,
(Self::AdverseExtreme, Side::Buy) => bar.high,
(Self::AdverseExtreme, Side::Sell) => bar.low,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn the_next_bar_price_is_its_open_or_its_worst_for_the_side() {
let bar = MarketBar::new(d("100"), d("110"), d("90"), d("105"));
assert_eq!(NextBarPrice::Open.reference(Side::Buy, &bar), d("100"));
assert_eq!(NextBarPrice::Open.reference(Side::Sell, &bar), d("100"));
assert_eq!(
NextBarPrice::AdverseExtreme.reference(Side::Buy, &bar),
d("110")
);
assert_eq!(
NextBarPrice::AdverseExtreme.reference(Side::Sell, &bar),
d("90")
);
}
#[test]
fn market_order_timing_defaults_to_on_arrival_and_round_trips() {
assert_eq!(MarketOrderTiming::default(), MarketOrderTiming::OnArrival);
for timing in [
MarketOrderTiming::OnArrival,
MarketOrderTiming::NextBar(NextBarPrice::Open),
MarketOrderTiming::NextBar(NextBarPrice::AdverseExtreme),
] {
let json = serde_json::to_string(&timing).unwrap();
assert_eq!(
serde_json::from_str::<MarketOrderTiming>(&json).unwrap(),
timing
);
}
}
#[test]
fn stale_market_policy_defaults_to_off_and_round_trips() {
assert_eq!(StaleMarketPolicy::default(), StaleMarketPolicy::Off);
let wait = StaleMarketPolicy::Wait {
max_age: Duration::from_secs(90),
};
assert_eq!(
serde_json::to_string(&wait).unwrap(),
r#"{"Wait":{"max_age":{"secs":90,"nanos":0}}}"#,
"the shape the rustdoc promises a configuration file"
);
for policy in [StaleMarketPolicy::Off, wait] {
let json = serde_json::to_string(&policy).unwrap();
assert_eq!(
serde_json::from_str::<StaleMarketPolicy>(&json).unwrap(),
policy
);
}
}
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, Decimal::ONE, &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, Decimal::ONE, &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, Decimal::ONE, &market)),
Some(d("99.5"))
);
}
#[test]
fn midpoint_uses_mid() {
let market = prices();
assert_eq!(
MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &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,
Decimal::ONE,
&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, Decimal::ONE, &market)),
LastPriceFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &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, Decimal::ONE, &market)),
BidAskFillModel.fill_price(&FillContext::new(Side::Sell, Decimal::ONE, &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, Decimal::ONE, &market)),
MidpointFillModel.fill_price(&FillContext::new(Side::Buy, Decimal::ONE, &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,
Decimal::ONE,
&snapshot(None, None, None)
)),
None
);
assert_eq!(
LastPriceFillModel.fill_price(&FillContext::new(
Side::Sell,
Decimal::ONE,
&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, Decimal::ONE, &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, Decimal::ONE, &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,
Decimal::ONE,
&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,
Decimal::ONE,
&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, Decimal::ONE, &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, Decimal::ONE, &empty);
assert_eq!(LastPriceFillModel.fill_price(&fill), None);
assert_eq!(BidAskFillModel.fill_price(&fill), None);
assert_eq!(MidpointFillModel.fill_price(&fill), None);
}
}
}