pub mod client_index;
pub mod fit;
pub mod grouped;
pub mod intent;
pub use client_index::ClientIndices;
pub use fit::{Fit, worst_price};
pub use grouped::{CreateGroupedOrders, Grouping, MOST_IN_GROUP};
pub use intent::{
DEFAULT_SLIPPAGE_BPS, Exit, MAX_SLIPPAGE_BPS, OrderIntent, Size, TRIGGER_SLIPPAGE_BPS, TWAP_LEAST_MS, TWAP_MOST_MS,
};
use crate::utils::json::Json;
use crate::utils::sign::goldilocks::Fp;
use crate::utils::sign::tx::{Attributes, BadTx, Tx, TxType, element, number};
use crate::utils::{ClientOrderIndex, Dec, Grid, MAX_SIZE_UNITS, MarketId, OrderKind, Side, TimeInForce};
const MOST_EXPIRY: u64 = i64::MAX.unsigned_abs();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Order {
pub market: MarketId,
pub client_index: Option<ClientOrderIndex>,
pub base_amount: u64,
pub price: u32,
pub side: Side,
pub kind: OrderKind,
pub time_in_force: TimeInForce,
pub reduce_only: bool,
pub trigger_price: u32,
pub expiry: u64,
}
impl Order {
#[must_use]
pub const fn with_client_index(mut self, index: ClientOrderIndex) -> Order {
self.client_index = Some(index);
self
}
#[must_use]
pub fn size(&self, grid: Grid) -> Dec {
grid.size_of(self.base_amount)
}
#[must_use]
pub fn limit(&self, grid: Grid) -> Dec {
grid.price_of(self.price)
}
#[must_use]
pub fn trigger(&self, grid: Grid) -> Option<Dec> {
(self.trigger_price != 0).then(|| grid.price_of(self.trigger_price))
}
#[must_use]
pub const fn takes(&self) -> bool {
match self.kind {
OrderKind::Limit => matches!(self.time_in_force, TimeInForce::ImmediateOrCancel),
OrderKind::StopLossLimit | OrderKind::TakeProfitLimit => false,
_ => true,
}
}
pub fn check(&self) -> Result<(), BadTx> {
self.check_fields()?;
let ioc = matches!(self.time_in_force, TimeInForce::ImmediateOrCancel);
let triggered = self.trigger_price != 0;
let expires = self.expiry != 0;
match self.kind {
OrderKind::Market => {
if !ioc {
return Err(BadTx::new("TimeInForce", "a market order is immediate-or-cancel"));
}
if expires {
return Err(BadTx::new("OrderExpiry", "a market order has no expiry"));
}
if triggered {
return Err(BadTx::new("TriggerPrice", "a market order has no trigger"));
}
}
OrderKind::Limit => {
if triggered {
return Err(BadTx::new("TriggerPrice", "a limit order has no trigger"));
}
if ioc && expires {
return Err(BadTx::new("OrderExpiry", "an immediate-or-cancel limit has no expiry"));
}
if !ioc && !expires {
return Err(BadTx::new("OrderExpiry", "a resting limit needs an expiry"));
}
}
kind if kind.is_trigger() => self.check_trigger()?,
OrderKind::Twap => {
if !matches!(self.time_in_force, TimeInForce::GoodTillTime) {
return Err(BadTx::new("TimeInForce", "a TWAP is good-till-time"));
}
if triggered {
return Err(BadTx::new("TriggerPrice", "a TWAP has no trigger"));
}
if !expires {
return Err(BadTx::new("OrderExpiry", "a TWAP needs the time it ends"));
}
}
_ => return Err(BadTx::new("Type", "the venue places TWAP slices and liquidations itself")),
}
Ok(())
}
pub fn check_fields(&self) -> Result<(), BadTx> {
if !self.reduce_only && self.base_amount == 0 {
return Err(BadTx::new("BaseAmount", "an order that is not reduce-only needs a size of at least 1"));
}
if self.base_amount > MAX_SIZE_UNITS {
return Err(BadTx::new("BaseAmount", "a size is at most 2^48 - 1 units"));
}
if self.price == 0 {
return Err(BadTx::new("Price", "a price is at least 1"));
}
if self.expiry > MOST_EXPIRY {
return Err(BadTx::new("OrderExpiry", "an expiry is unix milliseconds an int64 holds"));
}
Ok(())
}
pub(crate) fn check_trigger(&self) -> Result<(), BadTx> {
let market = matches!(self.kind, OrderKind::StopLoss | OrderKind::TakeProfit);
if market && !matches!(self.time_in_force, TimeInForce::ImmediateOrCancel) {
return Err(BadTx::new("TimeInForce", "a stop or take-profit that fires at market is immediate-or-cancel"));
}
if self.trigger_price == 0 {
return Err(BadTx::new("TriggerPrice", "a stop or take-profit needs a trigger"));
}
if self.expiry == 0 {
return Err(BadTx::new("OrderExpiry", "a stop or take-profit needs an expiry"));
}
Ok(())
}
pub(crate) fn hash_fields(&self) -> [Fp; 10] {
[
element(self.market.get()),
element(self.client_index.map_or(0, ClientOrderIndex::get)),
element(self.base_amount),
element(self.price),
element(u8::from(self.side.is_ask())),
element(self.kind.code()),
element(self.time_in_force.code()),
element(u8::from(self.reduce_only)),
element(self.trigger_price),
element(self.expiry),
]
}
pub(crate) fn info(&self) -> Vec<(&'static str, Json)> {
vec![
("MarketIndex", number(self.market.get())),
("ClientOrderIndex", number(self.client_index.map_or(0, ClientOrderIndex::get))),
("BaseAmount", number(self.base_amount)),
("Price", number(self.price)),
("IsAsk", number(u8::from(self.side.is_ask()))),
("Type", number(self.kind.code())),
("TimeInForce", number(self.time_in_force.code())),
("ReduceOnly", number(u8::from(self.reduce_only))),
("TriggerPrice", number(self.trigger_price)),
("OrderExpiry", number(self.expiry)),
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CreateOrder {
pub order: Order,
pub attributes: Attributes,
}
impl CreateOrder {
#[must_use]
pub const fn new(order: Order) -> CreateOrder {
CreateOrder { order, attributes: Attributes::NONE }
}
#[must_use]
pub const fn with(mut self, attributes: Attributes) -> CreateOrder {
self.attributes = attributes;
self
}
}
impl From<Order> for CreateOrder {
fn from(order: Order) -> CreateOrder {
CreateOrder::new(order)
}
}
impl Tx for CreateOrder {
fn tx_type(&self) -> TxType {
TxType::CreateOrder
}
fn hash_fields(&self) -> Vec<Fp> {
self.order.hash_fields().to_vec()
}
fn info_fields(&self) -> Vec<(&'static str, Json)> {
self.order.info()
}
fn attributes(&self) -> Attributes {
self.attributes
}
fn check(&self) -> Result<(), BadTx> {
self.attributes.check()?;
self.order.check()
}
}
#[cfg(test)]
#[path = "tests/create.rs"]
mod tests;