Skip to main content

df_hyperliquid_core/markets/
book.rs

1//! `l2Book`: a market's visible book, the same shape from `/info` and from the socket. Prices and
2//! sizes are exact [`Dec`] values read from the venue's strings.
3
4use serde::Deserialize;
5
6use crate::utils::dec::{Dec, Round};
7
8/// One price level: `n` orders resting at `px` for `sz` in all.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
10pub struct Level {
11    pub px: Dec,
12    pub sz: Dec,
13    /// Number of orders at this price, not an amount.
14    pub n: u32,
15}
16
17/// `l2Book`: at most twenty levels a side, best first. There is no sequence number; each read
18/// or frame replaces the last.
19#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
20#[serde(from = "Raw")]
21pub struct Book {
22    pub coin: String,
23    /// Milliseconds.
24    pub time: u64,
25    /// Highest first.
26    pub bids: Vec<Level>,
27    /// Lowest first.
28    pub asks: Vec<Level>,
29    /// Sent with a bucketed book (`nSigFigs`): the gap between the best levels.
30    pub spread: Option<Dec>,
31}
32
33/// `levels` on the wire: bids, then asks.
34#[derive(Deserialize)]
35struct Raw {
36    coin: String,
37    time: u64,
38    levels: (Vec<Level>, Vec<Level>),
39    #[serde(default)]
40    spread: Option<Dec>,
41}
42
43impl From<Raw> for Book {
44    fn from(raw: Raw) -> Self {
45        let (bids, asks) = raw.levels;
46        Book { coin: raw.coin, time: raw.time, bids, asks, spread: raw.spread }
47    }
48}
49
50impl Book {
51    #[must_use]
52    pub fn bids(&self) -> &[Level] {
53        &self.bids
54    }
55
56    #[must_use]
57    pub fn asks(&self) -> &[Level] {
58        &self.asks
59    }
60
61    /// The best bid and the best ask.
62    #[must_use]
63    pub fn touch(&self) -> (Option<Dec>, Option<Dec>) {
64        (self.bids.first().map(|level| level.px), self.asks.first().map(|level| level.px))
65    }
66
67    /// Exactly halfway between the best bid and ask, one place finer than either. `None` when a
68    /// side is empty.
69    #[must_use]
70    pub fn mid(&self) -> Option<Dec> {
71        let (Some(bid), Some(ask)) = self.touch() else { return None };
72        halfway(bid, ask)
73    }
74}
75
76/// Exactly halfway between two prices, one place finer than either.
77fn halfway(bid: Dec, ask: Dec) -> Option<Dec> {
78    let places = bid.places().max(ask.places()).saturating_add(1);
79    bid.checked_add(ask)?.checked_div(Dec::from_int(2), places, Round::Down)
80}
81
82#[cfg(test)]
83#[path = "tests/book.rs"]
84mod tests;