use crate::trade::error::TradeError;
use crate::trade::orders::fit::{Fit, worst_price};
use crate::trade::orders::{MOST_EXPIRY, Order};
use crate::utils::time::{DAY_MS, MAX_ORDER_EXPIRY_MS, MIN_ORDER_EXPIRY_MS, MINUTE_MS, RESTING_MS};
use crate::utils::{ClientOrderIndex, Dec, Grid, MarketId, OrderKind, Side, TimeInForce};
pub const DEFAULT_SLIPPAGE_BPS: u32 = 200;
pub const MAX_SLIPPAGE_BPS: u32 = 1_000;
pub const TRIGGER_SLIPPAGE_BPS: u32 = 200;
pub const TWAP_LEAST_MS: u64 = 5 * MINUTE_MS;
pub const TWAP_MOST_MS: u64 = DAY_MS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Size {
Base(Dec),
Quote(Dec),
Position,
}
impl From<Dec> for Size {
fn from(base: Dec) -> Size {
Size::Base(base)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Exit {
StopLoss,
TakeProfit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pricing {
Limit(Dec),
Market,
MarketAt(Dec),
Trigger {
exit: Exit,
trigger: Dec,
limit: Option<Dec>,
},
Twap {
worst: Dec,
until: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderIntent {
market: MarketId,
side: Side,
size: Size,
pricing: Pricing,
tif: Option<TimeInForce>,
expiry: Option<u64>,
slippage_bps: Option<u32>,
reference: Option<Dec>,
reduce_only: bool,
client_index: Option<ClientOrderIndex>,
fit: Fit,
least: Option<(Dec, Dec)>,
}
impl OrderIntent {
fn new(market: MarketId, side: Side, size: Size, pricing: Pricing) -> OrderIntent {
OrderIntent {
market,
side,
size,
pricing,
tif: None,
expiry: None,
slippage_bps: None,
reference: None,
reduce_only: false,
client_index: None,
fit: Fit::Exact,
least: None,
}
}
pub fn limit(market: MarketId, side: Side, size: impl Into<Size>, price: Dec) -> OrderIntent {
OrderIntent::new(market, side, size.into(), Pricing::Limit(price))
}
pub fn market(market: MarketId, side: Side, size: impl Into<Size>, reference: Dec) -> OrderIntent {
let mut intent = OrderIntent::new(market, side, size.into(), Pricing::Market);
intent.reference = Some(reference);
intent
}
pub fn market_at(market: MarketId, side: Side, size: impl Into<Size>, worst: Dec) -> OrderIntent {
OrderIntent::new(market, side, size.into(), Pricing::MarketAt(worst))
}
pub fn stop_loss(market: MarketId, side: Side, size: impl Into<Size>, trigger: Dec) -> OrderIntent {
OrderIntent::trigger(market, side, size.into(), Exit::StopLoss, trigger, None)
}
pub fn stop_loss_limit(
market: MarketId,
side: Side,
size: impl Into<Size>,
trigger: Dec,
limit: Dec,
) -> OrderIntent {
OrderIntent::trigger(market, side, size.into(), Exit::StopLoss, trigger, Some(limit))
}
pub fn take_profit(market: MarketId, side: Side, size: impl Into<Size>, trigger: Dec) -> OrderIntent {
OrderIntent::trigger(market, side, size.into(), Exit::TakeProfit, trigger, None)
}
pub fn take_profit_limit(
market: MarketId,
side: Side,
size: impl Into<Size>,
trigger: Dec,
limit: Dec,
) -> OrderIntent {
OrderIntent::trigger(market, side, size.into(), Exit::TakeProfit, trigger, Some(limit))
}
pub fn trigger(
market: MarketId,
side: Side,
size: impl Into<Size>,
exit: Exit,
trigger: Dec,
limit: Option<Dec>,
) -> OrderIntent {
OrderIntent::new(market, side, size.into(), Pricing::Trigger { exit, trigger, limit })
}
pub fn twap(market: MarketId, side: Side, size: impl Into<Size>, worst: Dec, until: u64) -> OrderIntent {
OrderIntent::new(market, side, size.into(), Pricing::Twap { worst, until })
}
#[must_use]
pub const fn post_only(mut self) -> OrderIntent {
self.tif = Some(TimeInForce::PostOnly);
self
}
#[must_use]
pub const fn ioc(mut self) -> OrderIntent {
self.tif = Some(TimeInForce::ImmediateOrCancel);
self
}
#[must_use]
pub const fn good_till_time(mut self) -> OrderIntent {
self.tif = Some(TimeInForce::GoodTillTime);
self
}
#[must_use]
pub const fn expires_at(mut self, at: u64) -> OrderIntent {
self.expiry = Some(at);
self
}
#[must_use]
pub const fn reduce_only(mut self) -> OrderIntent {
self.reduce_only = true;
self
}
#[must_use]
pub const fn client_index(mut self, index: ClientOrderIndex) -> OrderIntent {
self.client_index = Some(index);
self
}
#[must_use]
pub const fn slippage_bps(mut self, bps: u32) -> OrderIntent {
self.slippage_bps = Some(bps);
self
}
#[must_use]
pub const fn reference(mut self, reference: Dec) -> OrderIntent {
self.reference = Some(reference);
self
}
#[must_use]
pub const fn fit(mut self, fit: Fit) -> OrderIntent {
self.fit = fit;
self
}
#[must_use]
pub const fn minimums(mut self, least_size: Dec, least_value: Dec) -> OrderIntent {
self.least = Some((least_size, least_value));
self
}
#[must_use]
pub const fn market_id(&self) -> MarketId {
self.market
}
#[must_use]
pub const fn side(&self) -> Side {
self.side
}
#[must_use]
pub const fn size(&self) -> Size {
self.size
}
pub fn build(&self, grid: Grid, now_ms: u64) -> Result<Order, TradeError> {
let (kind, time_in_force) = self.kind()?;
self.check_slippage()?;
let fit = self.fit;
let (price, trigger) = match self.pricing {
Pricing::Limit(price) => (fit.price(grid, price, self.side)?, None),
Pricing::Market => {
let reference = self.reference.ok_or(TradeError::NoPrice("a market order"))?;
let bps = self.slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS);
(fit_bound(grid, self.side, reference, bps)?, None)
}
Pricing::MarketAt(worst) => {
let price = fit.price(grid, worst, self.side)?;
if let Some(reference) = self.reference {
let most = worst_price(grid, self.side, reference, MAX_SLIPPAGE_BPS)?;
let worst = grid.price_of(price);
let too_far = match self.side {
Side::Bid => worst > most,
Side::Ask => worst < most,
};
if too_far {
return Err(TradeError::TooFar { worst, reference });
}
}
(price, None)
}
Pricing::Trigger { trigger, limit, .. } => {
let trigger = fit.trigger(grid, trigger)?;
let price = match limit {
Some(limit) => fit.price(grid, limit, self.side)?,
None => {
let bps = self.slippage_bps.unwrap_or(TRIGGER_SLIPPAGE_BPS);
fit_bound(grid, self.side, grid.price_of(trigger), bps)?
}
};
(price, Some(trigger))
}
Pricing::Twap { worst, .. } => (fit.price(grid, worst, self.side)?, None),
};
let base_amount = self.base_amount(grid, price, trigger)?;
let order = Order {
market: self.market,
client_index: self.client_index,
base_amount,
price,
side: self.side,
kind,
time_in_force,
reduce_only: self.reduce_only,
trigger_price: trigger.unwrap_or(0),
expiry: 0,
};
let order = Order { expiry: self.expiry(&order, now_ms)?, ..order };
self.check_minimums(&order, grid)?;
order.check()?;
Ok(order)
}
fn kind(&self) -> Result<(OrderKind, TimeInForce), TradeError> {
let only = |tif: TimeInForce, why: &'static str| match self.tif {
None => Ok(tif),
Some(asked) if asked == tif => Ok(tif),
Some(_) => Err(TradeError::NotFor(why)),
};
Ok(match self.pricing {
Pricing::Limit(_) => (OrderKind::Limit, self.tif.unwrap_or(TimeInForce::GoodTillTime)),
Pricing::Market | Pricing::MarketAt(_) => {
(OrderKind::Market, only(TimeInForce::ImmediateOrCancel, "a market order is immediate-or-cancel")?)
}
Pricing::Trigger { exit, limit: None, .. } => {
let kind = match exit {
Exit::StopLoss => OrderKind::StopLoss,
Exit::TakeProfit => OrderKind::TakeProfit,
};
(kind, only(TimeInForce::ImmediateOrCancel, "a stop or take-profit at market is immediate-or-cancel")?)
}
Pricing::Trigger { exit, limit: Some(_), .. } => {
let kind = match exit {
Exit::StopLoss => OrderKind::StopLossLimit,
Exit::TakeProfit => OrderKind::TakeProfitLimit,
};
(kind, self.tif.unwrap_or(TimeInForce::GoodTillTime))
}
Pricing::Twap { .. } => (OrderKind::Twap, only(TimeInForce::GoodTillTime, "a TWAP is good-till-time")?),
})
}
fn check_slippage(&self) -> Result<(), TradeError> {
let Some(bps) = self.slippage_bps else { return Ok(()) };
match self.pricing {
Pricing::Market | Pricing::Trigger { limit: None, .. } => {}
_ => return Err(TradeError::NotFor("slippage is for a market order or a stop or take-profit at market")),
}
match bps > MAX_SLIPPAGE_BPS {
true => Err(TradeError::Slippage(bps)),
false => Ok(()),
}
}
fn base_amount(&self, grid: Grid, price: u32, trigger: Option<u32>) -> Result<u64, TradeError> {
match self.size {
Size::Base(size) => self.fit.size(grid, size),
Size::Quote(quote) => {
let at = match self.pricing {
Pricing::Market => self.reference.ok_or(TradeError::NoPrice("a quote-sized market order"))?,
Pricing::Trigger { limit: None, .. } => grid.price_of(trigger.unwrap_or(price)),
_ => grid.price_of(price),
};
self.fit.size_for(grid, quote, at)
}
Size::Position if self.reduce_only => Ok(0),
Size::Position => Err(TradeError::NotFor("the whole position is a size for a reduce-only order")),
}
}
fn expiry(&self, order: &Order, now_ms: u64) -> Result<u64, TradeError> {
let immediate = matches!(order.kind, OrderKind::Market)
|| matches!((order.kind, order.time_in_force), (OrderKind::Limit, TimeInForce::ImmediateOrCancel));
if immediate {
return match self.expiry {
None => Ok(0),
Some(_) => Err(TradeError::NotFor("an order that fills at once has no expiry")),
};
}
if let Pricing::Twap { until, .. } = self.pricing {
if self.expiry.is_some() {
return Err(TradeError::NotFor("a TWAP lapses when it ends"));
}
return within("OrderExpiry", "a TWAP's end", until, now_ms, TWAP_LEAST_MS, TWAP_MOST_MS);
}
match self.expiry {
Some(at) => within("OrderExpiry", "an expiry", at, now_ms, MIN_ORDER_EXPIRY_MS, MAX_ORDER_EXPIRY_MS),
None => Ok(now_ms.saturating_add(RESTING_MS)),
}
}
fn check_minimums(&self, order: &Order, grid: Grid) -> Result<(), TradeError> {
let Some((least_size, least_value)) = self.least else { return Ok(()) };
if order.takes() || order.base_amount == 0 {
return Ok(());
}
let size = order.size(grid);
if size < least_size {
return Err(TradeError::BelowLeastSize { size, least: least_size });
}
let value = size.checked_mul(order.limit(grid)).ok_or(TradeError::NoPrice("the order's value"))?;
if value < least_value {
return Err(TradeError::BelowLeastValue { value, least: least_value });
}
Ok(())
}
}
fn fit_bound(grid: Grid, side: Side, reference: Dec, bps: u32) -> Result<u32, TradeError> {
let bound = worst_price(grid, side, reference, bps)?;
crate::trade::error::units("Price", grid.price_units(bound))
}
pub(crate) fn within(
field: &'static str,
what: &'static str,
at: u64,
now: u64,
least_ms: u64,
most_ms: u64,
) -> Result<u64, TradeError> {
match at.checked_sub(now) {
Some(ahead) if (least_ms..=most_ms).contains(&ahead) && at <= MOST_EXPIRY => Ok(at),
_ => Err(TradeError::Expiry { field, what, at, now, least_ms, most_ms }),
}
}
#[cfg(test)]
#[path = "tests/intent.rs"]
mod tests;