Skip to main content

df_hyperliquid_core/trade/
order.rs

1//! `order`: one or more orders, grouped, optionally through a builder. Keys go in the order the
2//! venue hashes: `a b p s r t c` for an order, then `grouping`, then `builder` last.
3
4use crate::trade::builder_fee::BuilderFee;
5use crate::utils::sign::{L1Action, Map, Value};
6use crate::utils::{Address, Asset, AssetKind, Dec};
7
8/// Which way an order trades, the same `B` and `A` the reads and the socket carry.
9pub use crate::markets::Side;
10
11/// How long an order may rest, which way a trigger fires, and a client order id, the same types
12/// the orders read back carry.
13pub use crate::account::order::{Cloid, Tif, Tpsl};
14
15/// How the orders of one action relate.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum Grouping {
18    /// Each order stands alone.
19    #[default]
20    Na,
21    /// An entry and its take profit and stop loss.
22    NormalTpsl,
23    /// A take profit or stop loss on the whole position.
24    PositionTpsl,
25}
26
27impl Grouping {
28    #[must_use]
29    pub const fn wire(self) -> &'static str {
30        match self {
31            Grouping::Na => "na",
32            Grouping::NormalTpsl => "normalTpsl",
33            Grouping::PositionTpsl => "positionTpsl",
34        }
35    }
36}
37
38/// What an order's `t` says.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum OrderType {
41    Limit(Tif),
42    /// Rests off the book until the mark crosses `trigger`; then a market
43    /// order bounded by the order's price (`is_market`), or a limit at it.
44    Trigger {
45        is_market: bool,
46        trigger: Dec,
47        tpsl: Tpsl,
48    },
49}
50
51impl OrderType {
52    fn wire(self) -> Map {
53        match self {
54            OrderType::Limit(tif) => Map::new().put("limit", Map::new().put("tif", tif.wire())),
55            OrderType::Trigger { is_market, trigger, tpsl } => Map::new().put(
56                "trigger",
57                Map::new().put("isMarket", is_market).put("triggerPx", trigger.to_wire()).put("tpsl", tpsl.wire()),
58            ),
59        }
60    }
61}
62
63/// One order on any market. Price and size go on the wire exactly as given,
64/// so put them on the asset's grid first ([`crate::utils::Grid`]).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct Order {
67    asset: Asset,
68    side: Side,
69    price: Dec,
70    size: Dec,
71    reduce_only: bool,
72    order_type: OrderType,
73    cloid: Option<Cloid>,
74}
75
76impl Order {
77    /// A limit order that rests or crosses as `tif` says.
78    #[must_use]
79    pub const fn limit(asset: Asset, side: Side, price: Dec, size: Dec, tif: Tif) -> Order {
80        Order { asset, side, price, size, reduce_only: false, order_type: OrderType::Limit(tif), cloid: None }
81    }
82
83    /// A market order: `FrontendMarket`, always, at `bound`, the worst price
84    /// the trader takes (the reference moved by the slippage, on the grid; on
85    /// an outcome, the edge of the range). Never `Gtc`: a `Gtc` at the bound
86    /// would rest on the book for whatever did not fill.
87    #[must_use]
88    pub const fn market(asset: Asset, side: Side, bound: Dec, size: Dec) -> Order {
89        Order::limit(asset, side, bound, size, Tif::FrontendMarket)
90    }
91
92    /// A take profit or stop loss: fires when the mark crosses `trigger`, as
93    /// a market order bounded by `price` or as a limit at it. A size of zero
94    /// on a reduce-only trigger means the whole position.
95    #[must_use]
96    pub const fn trigger(
97        asset: Asset,
98        side: Side,
99        price: Dec,
100        size: Dec,
101        trigger: Dec,
102        tpsl: Tpsl,
103        is_market: bool,
104    ) -> Order {
105        Order {
106            asset,
107            side,
108            price,
109            size,
110            reduce_only: false,
111            order_type: OrderType::Trigger { is_market, trigger, tpsl },
112            cloid: None,
113        }
114    }
115
116    /// Only ever shrinks the position.
117    #[must_use]
118    pub const fn reduce_only(mut self) -> Order {
119        self.reduce_only = true;
120        self
121    }
122
123    #[must_use]
124    pub const fn with_cloid(mut self, cloid: Cloid) -> Order {
125        self.cloid = Some(cloid);
126        self
127    }
128
129    #[must_use]
130    pub const fn asset(&self) -> Asset {
131        self.asset
132    }
133
134    #[must_use]
135    pub const fn side(&self) -> Side {
136        self.side
137    }
138
139    #[must_use]
140    pub const fn price(&self) -> Dec {
141        self.price
142    }
143
144    #[must_use]
145    pub const fn size(&self) -> Dec {
146        self.size
147    }
148
149    #[must_use]
150    pub const fn is_reduce_only(&self) -> bool {
151        self.reduce_only
152    }
153
154    #[must_use]
155    pub const fn order_type(&self) -> OrderType {
156        self.order_type
157    }
158
159    #[must_use]
160    pub const fn cloid(&self) -> Option<Cloid> {
161        self.cloid
162    }
163
164    /// A limit order's TIF; `None` for a trigger.
165    #[must_use]
166    pub const fn tif(&self) -> Option<Tif> {
167        match self.order_type {
168            OrderType::Limit(tif) => Some(tif),
169            OrderType::Trigger { .. } => None,
170        }
171    }
172
173    #[must_use]
174    pub const fn is_market(&self) -> bool {
175        matches!(self.order_type, OrderType::Limit(Tif::FrontendMarket))
176    }
177
178    #[must_use]
179    pub const fn is_trigger(&self) -> bool {
180        matches!(self.order_type, OrderType::Trigger { .. })
181    }
182
183    /// `a b p s r t c`, in the order the venue hashes; `c` only when there is one.
184    #[must_use]
185    pub fn wire(&self) -> Map {
186        Map::new()
187            .put("a", self.asset)
188            .put("b", self.side.is_buy())
189            .put("p", self.price.to_wire())
190            .put("s", self.size.to_wire())
191            .put("r", self.reduce_only)
192            .put("t", self.order_type.wire())
193            .put_some("c", self.cloid.map(Cloid::to_hex))
194    }
195}
196
197/// Who routed the order and what it takes: `{b, f}`, the address lowercase,
198/// the fee in tenths of a basis point.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct Builder {
201    address: Address,
202    fee: BuilderFee,
203}
204
205impl Builder {
206    /// The account must have approved at least `fee` for this builder
207    /// (`approveBuilderFee`), or the venue refuses the order.
208    #[must_use]
209    pub const fn new(address: Address, fee: BuilderFee) -> Builder {
210        Builder { address, fee }
211    }
212
213    #[must_use]
214    pub const fn address(&self) -> Address {
215        self.address
216    }
217
218    #[must_use]
219    pub const fn fee(&self) -> BuilderFee {
220        self.fee
221    }
222
223    #[must_use]
224    pub fn wire(&self) -> Map {
225        Map::new().put("b", self.address.to_hex()).put("f", self.fee.tenths())
226    }
227}
228
229/// A builder fee over what the venue lets a builder take on these orders.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
231#[error("the builder fee of {} is over the {} {} order allows ({})", .fee.tenths(), .cap.tenths(), .kind.one(), .cap)]
232pub struct OverCap {
233    pub fee: BuilderFee,
234    pub cap: BuilderFee,
235    pub kind: AssetKind,
236}
237
238/// `order`: one or more orders, grouped, optionally through a builder.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct PlaceOrder {
241    orders: Vec<Order>,
242    grouping: Grouping,
243    builder: Option<Builder>,
244}
245
246impl PlaceOrder {
247    /// A scale is one action with an order a level.
248    #[must_use]
249    pub fn new(orders: Vec<Order>) -> PlaceOrder {
250        PlaceOrder { orders, grouping: Grouping::Na, builder: None }
251    }
252
253    #[must_use]
254    pub fn one(order: Order) -> PlaceOrder {
255        PlaceOrder::new(vec![order])
256    }
257
258    #[must_use]
259    pub fn grouped(mut self, grouping: Grouping) -> PlaceOrder {
260        self.grouping = grouping;
261        self
262    }
263
264    #[must_use]
265    pub fn through(mut self, builder: Builder) -> PlaceOrder {
266        self.builder = Some(builder);
267        self
268    }
269
270    #[must_use]
271    pub fn orders(&self) -> &[Order] {
272        &self.orders
273    }
274
275    #[must_use]
276    pub const fn grouping(&self) -> Grouping {
277        self.grouping
278    }
279
280    #[must_use]
281    pub const fn builder(&self) -> Option<Builder> {
282        self.builder
283    }
284
285    /// The builder's fee, when it is more than the venue lets a builder take
286    /// on these orders. One perp order holds the whole action to the perp cap.
287    #[must_use]
288    pub fn over_builder_cap(&self) -> Option<OverCap> {
289        let fee = self.builder?.fee;
290        let kind = AssetKind::strictest(self.orders.iter().map(Order::asset))?;
291        let cap = kind.builder_cap();
292        (fee > cap).then_some(OverCap { fee, cap, kind })
293    }
294}
295
296impl L1Action for PlaceOrder {
297    fn action(&self) -> Map {
298        Map::new()
299            .put("type", "order")
300            .put("orders", Value::array(self.orders.iter().map(Order::wire)))
301            .put("grouping", self.grouping.wire())
302            .put_some("builder", self.builder.as_ref().map(Builder::wire))
303    }
304}
305
306#[cfg(test)]
307#[path = "tests/order.rs"]
308mod tests;