Skip to main content

df_hyperliquid_core/live/
account.rs

1//! What the websocket says about one account: fills, orders, events, funding, the ledger, each
2//! dex's clearinghouse, spot balances, TWAPs, and what it may trade. Amounts are exact [`Dec`];
3//! the venue writes them as strings. Frames that open with history say so in `is_snapshot`.
4
5use std::fmt;
6
7use crate::live::market::Side;
8use crate::utils::{Address, Dec};
9use serde::{Deserialize, Deserializer, de};
10use serde_json::value::RawValue;
11
12/// One fill, as `userFills`, `userEvents` and the TWAP feeds send it, and a TWAP slice's fill.
13pub use crate::account::fill::{Fill, FillLiquidation, TwapSliceFill};
14
15/// `userFills`: the last fills first (`is_snapshot`), then each new one as it happens.
16#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct UserFills {
19    pub user: Address,
20    #[serde(default)]
21    pub is_snapshot: bool,
22    pub fills: Vec<Fill>,
23}
24
25/// An order as `orderUpdates` sends it, the shape of an `openOrders` row.
26pub use crate::account::order::OpenOrder as Order;
27
28/// One order's new status on `orderUpdates`: `open`, `filled`, `canceled`, `triggered`,
29/// `rejected`, `marginCanceled`, `badAloPxRejected` and the rest the info docs list.
30#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct OrderUpdate {
33    pub order: Order,
34    pub status: String,
35    /// Milliseconds.
36    pub status_timestamp: u64,
37}
38
39/// A resting order with its trigger and type, as `openOrders` and `userHistoricalOrders` send it,
40/// the shape of a `frontendOpenOrders` row.
41pub use crate::account::order::FrontendOrder as OpenOrder;
42
43/// `userEvents`, on the channel `user`: one kind of event a frame.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum UserEvent {
46    Fills(Vec<Fill>),
47    Funding(Funding),
48    Liquidation(Liquidation),
49    /// Orders the venue canceled, not the account.
50    NonUserCancel(Vec<Cancel>),
51    TwapHistory(Vec<TwapEvent>),
52    TwapSliceFills(Vec<TwapSliceFill>),
53    /// A kind this build does not read: its key, and its value as written.
54    Other {
55        key: String,
56        data: String,
57    },
58}
59
60impl<'de> Deserialize<'de> for UserEvent {
61    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62        struct Event;
63
64        impl<'de> de::Visitor<'de> for Event {
65            type Value = UserEvent;
66
67            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68                f.write_str("an object with one event in it")
69            }
70
71            fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<UserEvent, A::Error> {
72                let key: String = map.next_key()?.ok_or_else(|| de::Error::custom("an event with nothing in it"))?;
73                let event = match key.as_str() {
74                    "fills" => UserEvent::Fills(map.next_value()?),
75                    "funding" => UserEvent::Funding(map.next_value()?),
76                    "liquidation" => UserEvent::Liquidation(map.next_value()?),
77                    "nonUserCancel" => UserEvent::NonUserCancel(map.next_value()?),
78                    "twapHistory" => UserEvent::TwapHistory(map.next_value()?),
79                    "twapSliceFills" => UserEvent::TwapSliceFills(map.next_value()?),
80                    _ => {
81                        let data: Box<RawValue> = map.next_value()?;
82                        UserEvent::Other { key, data: data.get().to_owned() }
83                    }
84                };
85                if map.next_key::<de::IgnoredAny>()?.is_some() {
86                    return Err(de::Error::custom("more than one event in a frame"));
87                }
88                Ok(event)
89            }
90        }
91
92        deserializer.deserialize_map(Event)
93    }
94}
95
96/// One funding payment: on `userFundings`, and as a `userEvents` event on the hour.
97#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct Funding {
100    /// Milliseconds.
101    pub time: u64,
102    pub coin: String,
103    /// Paid out (negative) or received.
104    pub usdc: Dec,
105    /// The position it was paid on, signed.
106    pub szi: Dec,
107    pub funding_rate: Dec,
108    #[serde(default)]
109    pub n_samples: Option<u64>,
110}
111
112/// A liquidation the account took part in. The venue writes these keys in snake case.
113#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
114pub struct Liquidation {
115    pub lid: u64,
116    pub liquidator: Address,
117    pub liquidated_user: Address,
118    pub liquidated_ntl_pos: Dec,
119    pub liquidated_account_value: Dec,
120}
121
122/// An order the venue canceled.
123#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
124pub struct Cancel {
125    pub coin: String,
126    pub oid: u64,
127}
128
129/// `userFundings`: past payments first (`is_snapshot`), then one an hour per position.
130#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct UserFundings {
133    pub user: Address,
134    #[serde(default)]
135    pub is_snapshot: bool,
136    pub fundings: Vec<Funding>,
137}
138
139/// `userNonFundingLedgerUpdates`: deposits, withdrawals, transfers, sends, vault moves and
140/// liquidations; past ones first (`is_snapshot`).
141#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Ledger {
144    pub user: Address,
145    #[serde(default)]
146    pub is_snapshot: bool,
147    #[serde(rename = "nonFundingLedgerUpdates")]
148    pub updates: Vec<LedgerUpdate>,
149}
150
151/// One ledger row, as `userNonFundingLedgerUpdates` answers it. A type this build does not read is
152/// kept as written.
153pub use crate::account::ledger::{Delta, LedgerUpdate};
154
155/// One dex's clearinghouse, its margin and positions, a position's leverage (or what the account
156/// has set on a coin) and whether it shares the account's balance, as `clearinghouseState` answers.
157pub use crate::account::clearinghouse::{
158    AssetPosition, Clearinghouse, CumFunding, Leverage, MarginMode as Margin, MarginSummary, Position,
159};
160
161/// `clearinghouseState`: one dex's clearinghouse (`dex` empty for the first).
162#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct DexState {
165    pub dex: String,
166    pub user: Address,
167    #[serde(rename = "clearinghouseState")]
168    pub state: Clearinghouse,
169}
170
171/// `allDexsClearinghouseState`: every dex's clearinghouse, the first named `""`.
172#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct AllDexsState {
175    pub user: Address,
176    #[serde(rename = "clearinghouseStates")]
177    pub states: Vec<(String, Clearinghouse)>,
178}
179
180/// `openOrders`: every resting order on one dex.
181#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
182pub struct OpenOrders {
183    pub dex: String,
184    pub user: Address,
185    pub orders: Vec<OpenOrder>,
186}
187
188/// `spotState`: spot balances, outcome sides among them (`+14720`, with no token).
189#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct SpotState {
192    pub user: Address,
193    /// The shape `spotClearinghouseState` answers.
194    #[serde(rename = "spotState")]
195    pub state: crate::account::clearinghouse::SpotState,
196}
197
198/// One token's balance. `total` already includes `hold`; under portfolio margin a hold may be
199/// negative, room to borrow. A row of `spotClearinghouseState`.
200pub use crate::account::clearinghouse::SpotBalance as Balance;
201
202/// `twapStates`: the account's running TWAPs on one dex, by id.
203#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
204pub struct TwapStates {
205    pub dex: String,
206    pub user: Address,
207    pub states: Vec<(u64, TwapState)>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct TwapState {
213    pub coin: String,
214    pub user: Address,
215    pub side: Side,
216    pub sz: Dec,
217    pub executed_sz: Dec,
218    pub executed_ntl: Dec,
219    pub minutes: u32,
220    pub reduce_only: bool,
221    pub randomize: bool,
222    /// When it started, milliseconds.
223    pub timestamp: u64,
224}
225
226/// `userTwapSliceFills`: past slices first (`is_snapshot`), then each as it fills.
227#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct TwapSliceFills {
230    pub user: Address,
231    #[serde(default)]
232    pub is_snapshot: bool,
233    #[serde(rename = "twapSliceFills")]
234    pub fills: Vec<TwapSliceFill>,
235}
236
237/// A TWAP starting, finishing, being stopped or failing.
238#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct TwapEvent {
241    /// Seconds, not milliseconds: the venue writes this one so.
242    pub time: u64,
243    pub state: TwapState,
244    pub status: TwapStatus,
245    #[serde(default)]
246    pub twap_id: Option<u64>,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
250pub struct TwapStatus {
251    /// `activated`, `finished`, `terminated` or `error`.
252    pub status: String,
253    /// Why, on an error.
254    #[serde(default)]
255    pub description: Option<String>,
256}
257
258/// `userTwapHistory`: past TWAP events first (`is_snapshot`), then each as it happens.
259#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct TwapHistory {
262    pub user: Address,
263    #[serde(default)]
264    pub is_snapshot: bool,
265    pub history: Vec<TwapEvent>,
266}
267
268/// `webData3`: the account as Hyperliquid's own frontend reads it. The docs warn that fields they do
269/// not list may go; only the listed ones and `abstraction` are read.
270#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct WebData3 {
273    pub user_state: UserState,
274    pub perp_dex_states: Vec<PerpDexState>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct UserState {
280    pub user: Address,
281    #[serde(default)]
282    pub agent_address: Option<Address>,
283    /// Milliseconds.
284    #[serde(default)]
285    pub agent_valid_until: Option<u64>,
286    /// Milliseconds.
287    pub server_time: u64,
288    pub cum_ledger: Dec,
289    pub is_vault: bool,
290    /// The account mode: `disabled`, `default`, `unifiedAccount`, `portfolioMargin`.
291    #[serde(default)]
292    pub abstraction: Option<String>,
293    #[serde(default)]
294    pub dex_abstraction_enabled: bool,
295    #[serde(default)]
296    pub opt_out_of_spot_dusting: bool,
297}
298
299/// One perp dex, in the order the dexes are listed.
300#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct PerpDexState {
303    pub total_vault_equity: Dec,
304    #[serde(default)]
305    pub perps_at_open_interest_cap: Vec<String>,
306    #[serde(default)]
307    pub leading_vaults: Vec<LeadingVault>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
311pub struct LeadingVault {
312    pub address: Address,
313    pub name: String,
314}
315
316/// `activeAssetData`: what the account may trade on one perp, buy then sell.
317pub use crate::account::clearinghouse::ActiveAssetData as AssetData;
318
319/// `userHistoricalOrders`: orders and what became of them, the latest first.
320#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct HistoricalOrders {
323    pub user: Address,
324    #[serde(default)]
325    pub is_snapshot: bool,
326    #[serde(rename = "orderHistory")]
327    pub orders: Vec<HistoricalOrder>,
328}
329
330/// An order and what became of it, the shape of a `historicalOrders` row.
331pub use crate::account::order::OrderRecord as HistoricalOrder;