df_hyperliquid_core/utils/
rounding.rs1use crate::trade::error::TradeError;
7use crate::trade::order::{Order, Side, Tif};
8use crate::utils::{Asset, Dec, Round};
9
10pub const SIGNIFICANT_FIGURES: u32 = 5;
12
13pub const PERP_PRICE_PLACES: u32 = 6;
15
16pub const SPOT_PRICE_PLACES: u32 = 8;
18
19pub const MINIMUM_ORDER_VALUE: Dec = Dec::from_int(10);
21
22pub const MAX_SLIPPAGE: Dec = Dec::lit::<1>(5);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct Grid {
29 sz_decimals: u32,
30 ceiling: u32,
31}
32
33impl Grid {
34 #[must_use]
36 pub const fn perp(sz_decimals: u32) -> Grid {
37 Grid { sz_decimals, ceiling: PERP_PRICE_PLACES }
38 }
39
40 #[must_use]
42 pub const fn spot(sz_decimals: u32) -> Grid {
43 Grid { sz_decimals, ceiling: SPOT_PRICE_PLACES }
44 }
45
46 #[must_use]
47 pub const fn sz_decimals(self) -> u32 {
48 self.sz_decimals
49 }
50
51 #[must_use]
54 pub const fn price_places(self) -> u32 {
55 self.ceiling.saturating_sub(self.sz_decimals)
56 }
57
58 #[must_use]
64 pub fn tick(self, price: Dec, round: Round) -> Option<Dec> {
65 if !price.is_positive() {
66 return None;
67 }
68 let places = self.price_places().min(price.sig_places(SIGNIFICANT_FIGURES));
69 let kept = price.round(places, round);
70 kept.is_positive().then_some(kept)
71 }
72
73 #[must_use]
76 pub fn price(self, price: Dec, side: Side) -> Option<Dec> {
77 self.tick(price, side.price_rounding())
78 }
79
80 #[must_use]
82 pub fn is_on_grid(self, price: Dec) -> bool {
83 self.tick(price, Round::Down) == Some(price)
84 }
85
86 #[must_use]
89 pub fn size(self, size: Dec) -> Option<Dec> {
90 if !size.is_positive() {
91 return None;
92 }
93 let kept = size.round(self.sz_decimals, Round::Down);
94 kept.is_positive().then_some(kept)
95 }
96
97 pub fn market_bound(self, reference: Dec, slippage: Dec, side: Side) -> Result<Dec, TradeError> {
101 if slippage.is_negative() || slippage > MAX_SLIPPAGE {
102 return Err(TradeError::Slippage(slippage));
103 }
104 let factor = match side {
105 Side::Buy => Dec::ONE.checked_add(slippage),
106 Side::Sell => Dec::ONE.checked_sub(slippage),
107 };
108 factor
109 .and_then(|factor| reference.checked_mul(factor))
110 .and_then(|moved| self.price(moved, side))
111 .ok_or(TradeError::NoPrice(reference))
112 }
113
114 pub fn limit(self, asset: Asset, side: Side, price: Dec, size: Dec, tif: Tif) -> Result<Order, TradeError> {
117 let price = self.price(price, side).ok_or(TradeError::NoPrice(price))?;
118 let size = self.size(size).ok_or(TradeError::NoSize(size))?;
119 Ok(Order::limit(asset, side, price, size, tif))
120 }
121
122 pub fn market(
125 self,
126 asset: Asset,
127 side: Side,
128 reference: Dec,
129 slippage: Dec,
130 size: Dec,
131 ) -> Result<Order, TradeError> {
132 let bound = self.market_bound(reference, slippage, side)?;
133 let size = self.size(size).ok_or(TradeError::NoSize(size))?;
134 Ok(Order::market(asset, side, bound, size))
135 }
136
137 #[must_use]
140 pub fn least_size(self, price: Dec) -> Option<Dec> {
141 let exact = MINIMUM_ORDER_VALUE.checked_div(price, 12, Round::Up)?;
142 let size = exact.round(self.sz_decimals, Round::Up);
143 size.is_positive().then_some(size)
144 }
145
146 #[must_use]
148 pub fn meets_minimum(price: Dec, size: Dec) -> bool {
149 price.checked_mul(size).is_some_and(|worth| worth >= MINIMUM_ORDER_VALUE)
150 }
151}
152
153#[cfg(test)]
154#[path = "tests/rounding.rs"]
155mod tests;