use crate::trade::error::TradeError;
use crate::trade::order::{Order, Side, Tif};
use crate::utils::{Asset, Dec, Round};
pub const SIGNIFICANT_FIGURES: u32 = 5;
pub const PERP_PRICE_PLACES: u32 = 6;
pub const SPOT_PRICE_PLACES: u32 = 8;
pub const MINIMUM_ORDER_VALUE: Dec = Dec::from_int(10);
pub const MAX_SLIPPAGE: Dec = Dec::lit::<1>(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Grid {
sz_decimals: u32,
ceiling: u32,
}
impl Grid {
#[must_use]
pub const fn perp(sz_decimals: u32) -> Grid {
Grid { sz_decimals, ceiling: PERP_PRICE_PLACES }
}
#[must_use]
pub const fn spot(sz_decimals: u32) -> Grid {
Grid { sz_decimals, ceiling: SPOT_PRICE_PLACES }
}
#[must_use]
pub const fn sz_decimals(self) -> u32 {
self.sz_decimals
}
#[must_use]
pub const fn price_places(self) -> u32 {
self.ceiling.saturating_sub(self.sz_decimals)
}
#[must_use]
pub fn tick(self, price: Dec, round: Round) -> Option<Dec> {
if !price.is_positive() {
return None;
}
let places = self.price_places().min(price.sig_places(SIGNIFICANT_FIGURES));
let kept = price.round(places, round);
kept.is_positive().then_some(kept)
}
#[must_use]
pub fn price(self, price: Dec, side: Side) -> Option<Dec> {
self.tick(price, side.price_rounding())
}
#[must_use]
pub fn is_on_grid(self, price: Dec) -> bool {
self.tick(price, Round::Down) == Some(price)
}
#[must_use]
pub fn size(self, size: Dec) -> Option<Dec> {
if !size.is_positive() {
return None;
}
let kept = size.round(self.sz_decimals, Round::Down);
kept.is_positive().then_some(kept)
}
pub fn market_bound(self, reference: Dec, slippage: Dec, side: Side) -> Result<Dec, TradeError> {
if slippage.is_negative() || slippage > MAX_SLIPPAGE {
return Err(TradeError::Slippage(slippage));
}
let factor = match side {
Side::Buy => Dec::ONE.checked_add(slippage),
Side::Sell => Dec::ONE.checked_sub(slippage),
};
factor
.and_then(|factor| reference.checked_mul(factor))
.and_then(|moved| self.price(moved, side))
.ok_or(TradeError::NoPrice(reference))
}
pub fn limit(self, asset: Asset, side: Side, price: Dec, size: Dec, tif: Tif) -> Result<Order, TradeError> {
let price = self.price(price, side).ok_or(TradeError::NoPrice(price))?;
let size = self.size(size).ok_or(TradeError::NoSize(size))?;
Ok(Order::limit(asset, side, price, size, tif))
}
pub fn market(
self,
asset: Asset,
side: Side,
reference: Dec,
slippage: Dec,
size: Dec,
) -> Result<Order, TradeError> {
let bound = self.market_bound(reference, slippage, side)?;
let size = self.size(size).ok_or(TradeError::NoSize(size))?;
Ok(Order::market(asset, side, bound, size))
}
#[must_use]
pub fn least_size(self, price: Dec) -> Option<Dec> {
let exact = MINIMUM_ORDER_VALUE.checked_div(price, 12, Round::Up)?;
let size = exact.round(self.sz_decimals, Round::Up);
size.is_positive().then_some(size)
}
#[must_use]
pub fn meets_minimum(price: Dec, size: Dec) -> bool {
price.checked_mul(size).is_some_and(|worth| worth >= MINIMUM_ORDER_VALUE)
}
}
#[cfg(test)]
#[path = "tests/rounding.rs"]
mod tests;