Skip to main content

df_hyperliquid_core/account/
order.rs

1//! An account's orders as the venue reads them back, on every market kind, the same shapes whether
2//! `/info` answers them or the socket sends them, and the words an order is placed with: its time
3//! in force, its trigger, its client id.
4
5use std::{borrow::Cow, fmt, str::FromStr};
6
7use serde::{Deserialize, Deserializer, de};
8
9use crate::markets::side::Side;
10use crate::utils::dec::Dec;
11use crate::utils::hex::{self, BadHex};
12
13/// How long an order may rest.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Tif {
16    /// Add liquidity only: refused if it would cross.
17    Alo,
18    /// Immediate or cancel.
19    Ioc,
20    /// Rests until filled or cancelled. Never a market order.
21    Gtc,
22    /// The venue's market order: crosses up to its price and never rests. Only
23    /// [`crate::trade::Order::market`] makes one.
24    FrontendMarket,
25}
26
27impl Tif {
28    #[must_use]
29    pub const fn wire(self) -> &'static str {
30        match self {
31            Tif::Alo => "Alo",
32            Tif::Ioc => "Ioc",
33            Tif::Gtc => "Gtc",
34            Tif::FrontendMarket => "FrontendMarket",
35        }
36    }
37}
38
39/// Which way a trigger fires.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum Tpsl {
42    /// When the price moves the position's way.
43    TakeProfit,
44    /// When it moves against it.
45    StopLoss,
46}
47
48impl Tpsl {
49    #[must_use]
50    pub const fn wire(self) -> &'static str {
51        match self {
52            Tpsl::TakeProfit => "tp",
53            Tpsl::StopLoss => "sl",
54        }
55    }
56}
57
58/// A client order id: 128 bits, written as `0x` and 32 hex digits.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct Cloid([u8; 16]);
61
62impl Cloid {
63    #[must_use]
64    pub const fn from_bytes(bytes: [u8; 16]) -> Cloid {
65        Cloid(bytes)
66    }
67
68    #[must_use]
69    pub const fn from_u128(value: u128) -> Cloid {
70        Cloid(value.to_be_bytes())
71    }
72
73    #[must_use]
74    pub const fn bytes(&self) -> &[u8; 16] {
75        &self.0
76    }
77
78    #[must_use]
79    pub fn to_hex(self) -> String {
80        hex::encode_prefixed(&self.0)
81    }
82}
83
84impl FromStr for Cloid {
85    type Err = BadHex;
86
87    fn from_str(text: &str) -> Result<Self, Self::Err> {
88        hex::decode_array(text).map(Cloid)
89    }
90}
91
92impl fmt::Display for Cloid {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.write_str(&self.to_hex())
95    }
96}
97
98/// From the hex the venue writes for an order's `cloid`.
99impl<'de> Deserialize<'de> for Cloid {
100    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101        <Cow<'de, str>>::deserialize(deserializer)?.parse().map_err(de::Error::custom)
102    }
103}
104
105/// One `openOrders` row, and the order an `orderUpdates` frame carries. `reduceOnly` comes only
106/// when set.
107#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct OpenOrder {
110    /// `BTC`, `xyz:XYZ100` on a HIP-3 dex, `@107` or `PURR/USDC` for a pair, `#14730`.
111    pub coin: String,
112    /// The side it rests on.
113    pub side: Side,
114    pub limit_px: Dec,
115    /// What is left; `0` on a position's take profit or stop loss.
116    pub sz: Dec,
117    pub oid: u64,
118    /// When it was placed, milliseconds.
119    pub timestamp: u64,
120    pub orig_sz: Dec,
121    #[serde(default)]
122    pub reduce_only: bool,
123    #[serde(default)]
124    pub cloid: Option<Cloid>,
125}
126
127/// What kind of order a `frontendOpenOrders` row is, in the venue's words.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum OrderKind {
130    Limit,
131    Market,
132    StopMarket,
133    StopLimit,
134    TakeProfitMarket,
135    TakeProfitLimit,
136    /// A kind this build does not know, kept as written.
137    Other(String),
138}
139
140impl OrderKind {
141    /// From the venue's words: `Stop Market`. A kind this build does not know is kept as
142    /// written.
143    #[must_use]
144    pub fn parse(word: &str) -> OrderKind {
145        match word {
146            "Limit" => OrderKind::Limit,
147            "Market" => OrderKind::Market,
148            "Stop Market" => OrderKind::StopMarket,
149            "Stop Limit" => OrderKind::StopLimit,
150            "Take Profit Market" => OrderKind::TakeProfitMarket,
151            "Take Profit Limit" => OrderKind::TakeProfitLimit,
152            other => OrderKind::Other(other.to_owned()),
153        }
154    }
155
156    /// As the venue writes it: `Stop Market`.
157    #[must_use]
158    pub fn word(&self) -> &str {
159        match self {
160            OrderKind::Limit => "Limit",
161            OrderKind::Market => "Market",
162            OrderKind::StopMarket => "Stop Market",
163            OrderKind::StopLimit => "Stop Limit",
164            OrderKind::TakeProfitMarket => "Take Profit Market",
165            OrderKind::TakeProfitLimit => "Take Profit Limit",
166            OrderKind::Other(word) => word,
167        }
168    }
169
170    /// Which trigger it is; `None` for a plain order.
171    #[must_use]
172    pub const fn tpsl(&self) -> Option<Tpsl> {
173        match self {
174            OrderKind::StopMarket | OrderKind::StopLimit => Some(Tpsl::StopLoss),
175            OrderKind::TakeProfitMarket | OrderKind::TakeProfitLimit => Some(Tpsl::TakeProfit),
176            _ => None,
177        }
178    }
179
180    /// Whether it goes at market (once it fires, for a trigger).
181    #[must_use]
182    pub const fn fills_at_market(&self) -> bool {
183        matches!(self, OrderKind::Market | OrderKind::StopMarket | OrderKind::TakeProfitMarket)
184    }
185}
186
187impl<'de> Deserialize<'de> for OrderKind {
188    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
189        Ok(OrderKind::parse(&<Cow<'de, str>>::deserialize(deserializer)?))
190    }
191}
192
193/// One `frontendOpenOrders` row, the order inside `orderStatus` and `historicalOrders`, and what
194/// the socket's `openOrders` and `userHistoricalOrders` send: an `openOrders` row with its
195/// trigger and what kind of order it is.
196#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct FrontendOrder {
199    pub coin: String,
200    pub side: Side,
201    pub limit_px: Dec,
202    /// What is left; `0` on a position TP/SL, which closes the whole position.
203    pub sz: Dec,
204    pub oid: u64,
205    /// Milliseconds.
206    pub timestamp: u64,
207    /// `N/A`, `Price above 6.04`, or `Triggered` once a trigger has fired.
208    pub trigger_condition: String,
209    pub is_trigger: bool,
210    /// `0.0` on an order that is not a trigger.
211    pub trigger_px: Dec,
212    /// The take profit and stop loss of an entry not yet filled.
213    #[serde(default)]
214    pub children: Vec<FrontendOrder>,
215    pub is_position_tpsl: bool,
216    pub reduce_only: bool,
217    pub order_type: OrderKind,
218    pub orig_sz: Dec,
219    /// As the venue writes it: `Gtc`, `Alo`, `Ioc`, `FrontendMarket`, `LiquidationMarket`;
220    /// `null` on a trigger still waiting.
221    #[serde(default)]
222    pub tif: Option<String>,
223    #[serde(default)]
224    pub cloid: Option<Cloid>,
225}
226
227impl FrontendOrder {
228    /// Where it fires, while it is a trigger waiting to.
229    #[must_use]
230    pub fn trigger(&self) -> Option<Dec> {
231        (self.is_trigger && self.trigger_px.is_positive()).then_some(self.trigger_px)
232    }
233
234    #[must_use]
235    pub const fn tpsl(&self) -> Option<Tpsl> {
236        self.order_type.tpsl()
237    }
238
239    /// The time in force, when it is one an order may be sent with.
240    #[must_use]
241    pub fn time_in_force(&self) -> Option<Tif> {
242        match self.tif.as_deref()? {
243            "Gtc" => Some(Tif::Gtc),
244            "Alo" => Some(Tif::Alo),
245            "Ioc" => Some(Tif::Ioc),
246            "FrontendMarket" => Some(Tif::FrontendMarket),
247            _ => None,
248        }
249    }
250}
251
252/// Where an order stands, by the kind of end the venue's word names. The word itself is kept on
253/// a cancel or a rejection: `marginCanceled`, `scheduledCancel` (the dead man's switch),
254/// `minTradeNtlRejected`.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum OrderState {
257    Open,
258    Filled,
259    Triggered,
260    Canceled(String),
261    Rejected(String),
262    /// A word this build does not know.
263    Other(String),
264}
265
266impl OrderState {
267    /// As the venue wrote it.
268    #[must_use]
269    pub fn word(&self) -> &str {
270        match self {
271            OrderState::Open => "open",
272            OrderState::Filled => "filled",
273            OrderState::Triggered => "triggered",
274            OrderState::Canceled(word) | OrderState::Rejected(word) | OrderState::Other(word) => word,
275        }
276    }
277}
278
279impl<'de> Deserialize<'de> for OrderState {
280    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
281        let word = String::deserialize(deserializer)?;
282        Ok(match word.as_str() {
283            "open" => OrderState::Open,
284            "filled" => OrderState::Filled,
285            "triggered" => OrderState::Triggered,
286            "canceled" | "scheduledCancel" => OrderState::Canceled(word),
287            canceled if canceled.ends_with("Canceled") => OrderState::Canceled(word),
288            rejected if rejected == "rejected" || rejected.ends_with("Rejected") => OrderState::Rejected(word),
289            _ => OrderState::Other(word),
290        })
291    }
292}
293
294impl fmt::Display for OrderState {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        f.write_str(self.word())
297    }
298}
299
300/// An order and where it stood at `status_timestamp`: an `orderStatus` answer's order, each
301/// `historicalOrders` row, and each order `userHistoricalOrders` sends.
302#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct OrderRecord {
305    pub order: FrontendOrder,
306    pub status: OrderState,
307    /// Milliseconds.
308    pub status_timestamp: u64,
309}