Skip to main content

df_hyperliquid_core/account/
limits.rs

1//! What an address may still send (`userRateLimit`) and how fast that runs down. Action limits are
2//! per address, a subaccount having its own; reads are the IP's ([`crate::client::limit`]).
3//! Nothing here keeps a clock: the time is passed in.
4
5use crate::client::{ApiError, Transport, info, user_request};
6use crate::utils::{Address, Dec, Round};
7use serde::Deserialize;
8
9/// What every address starts with; it earns one more request for every USDC it trades.
10pub const INITIAL_REQUESTS: u64 = 10_000;
11
12/// How often a throttled address may send one request.
13pub const THROTTLED_EVERY_MS: u64 = 10_000;
14
15/// The open orders every address has.
16pub const OPEN_ORDERS: u64 = 1_000;
17/// The most open orders an address can earn.
18pub const MOST_OPEN_ORDERS: u64 = 5_000;
19
20/// The volume that earns one more open order, in USDC.
21pub const VOLUME_PER_OPEN_ORDER: i64 = 5_000_000;
22
23/// The least time between two reads of the same limits, and the least time
24/// a pace is measured over.
25pub const READ_EVERY_MS: u64 = 60_000;
26
27/// How many limits to read at a time; each read weighs 20 against the IP and nothing against the
28/// address.
29pub const READ_BATCH: usize = 10;
30
31/// `{"type":"userRateLimit","user":..}`.
32#[must_use]
33pub fn rate_limit_request(user: Address) -> String {
34    user_request("userRateLimit", user)
35}
36
37/// One address's limit. Weighs 20 against the IP.
38pub async fn rate_limit(transport: &impl Transport, user: Address) -> Result<RateLimit, ApiError> {
39    info(transport, rate_limit_request(user)).await
40}
41
42/// `userRateLimit`.
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
44pub struct RateLimit {
45    /// USDC traded since the address began: what buys its cap.
46    #[serde(rename = "cumVlm")]
47    pub volume: Dec,
48    #[serde(rename = "nRequestsUsed")]
49    pub used: u64,
50    #[serde(rename = "nRequestsCap")]
51    pub cap: u64,
52    /// Bought with `reserveRequestWeight`.
53    #[serde(rename = "nRequestsSurplus", default)]
54    pub surplus: u64,
55}
56
57impl RateLimit {
58    /// What is left before the address is throttled; never a wrapped total,
59    /// since throttled requests still count.
60    #[must_use]
61    pub const fn remaining(&self) -> u64 {
62        self.cap.saturating_sub(self.used)
63    }
64
65    #[must_use]
66    pub const fn is_throttled(&self) -> bool {
67        self.remaining() == 0
68    }
69
70    /// What cancels may use: `min(cap + 100000, cap * 2)`, so a throttled address can still cancel.
71    #[must_use]
72    pub const fn cancel_cap(&self) -> u64 {
73        let wider = self.cap.saturating_add(100_000);
74        let doubled = self.cap.saturating_mul(2);
75        if wider < doubled { wider } else { doubled }
76    }
77
78    /// The cap `volume` earns: 10,000 and one for every whole USDC. `None`
79    /// for a negative volume.
80    #[must_use]
81    pub fn earned_cap(volume: Dec) -> Option<u64> {
82        let whole = u64::try_from(volume.units_at(0, Round::Down)?).ok()?;
83        INITIAL_REQUESTS.checked_add(whole)
84    }
85}
86
87/// The USDC to trade to earn `requests` more.
88#[must_use]
89pub fn volume_for(requests: u64) -> Dec {
90    Dec::lit::<0>(i128::from(requests))
91}
92
93/// What `reserveRequestWeight` charges for `requests` more, in USDC: 0.0005 a request, paid from
94/// the perps balance.
95#[must_use]
96pub fn surplus_cost(requests: u64) -> Option<Dec> {
97    Dec::lit::<0>(i128::from(requests)).checked_mul(Dec::lit::<4>(5))
98}
99
100/// What an action costs the address: one, or one for every order or cancel it batches. The IP
101/// counts a batch as one.
102#[must_use]
103pub fn address_requests(batched: usize) -> u64 {
104    u64::try_from(batched.max(1)).unwrap_or(u64::MAX)
105}
106
107/// The open orders `volume` allows: one more for every 5M USDC, at most 5000.
108#[must_use]
109pub fn open_order_cap(volume: Dec) -> u64 {
110    let earned = volume
111        .checked_div(Dec::from_int(VOLUME_PER_OPEN_ORDER), 0, Round::Down)
112        .and_then(|earned| earned.units_at(0, Round::Down))
113        .and_then(|earned| u64::try_from(earned).ok())
114        .unwrap_or(0);
115    OPEN_ORDERS.saturating_add(earned).min(MOST_OPEN_ORDERS)
116}
117
118/// How long, from `read_at`, before limits may be read again; `None` when
119/// they may be read now. Milliseconds.
120#[must_use]
121pub fn read_wait_ms(read_at: Option<u64>, now: u64) -> Option<u64> {
122    let next = read_at?.saturating_add(READ_EVERY_MS);
123    (next > now).then(|| next - now)
124}
125
126/// One read of a limit, at `at` milliseconds.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct Sample {
129    pub at: u64,
130    pub limit: RateLimit,
131}
132
133/// One address's limit as last read, and the read its pace is measured from.
134#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct Budget {
136    pub last: Option<Sample>,
137    /// The latest read at least [`READ_EVERY_MS`] older than `last`.
138    pub base: Option<Sample>,
139    pub error: Option<String>,
140}
141
142impl Budget {
143    pub fn took(&mut self, sample: Sample) {
144        if let Some(last) = self.last
145            && sample.at.saturating_sub(last.at) >= READ_EVERY_MS
146        {
147            self.base = Some(last);
148        }
149        self.last = Some(sample);
150        self.error = None;
151    }
152
153    #[must_use]
154    pub fn limit(&self) -> Option<RateLimit> {
155        self.last.map(|sample| sample.limit)
156    }
157
158    /// Requests spent less those trading earned back, and the milliseconds
159    /// that took, between the two reads a pace is measured over.
160    fn spent(&self) -> Option<(i128, i128)> {
161        let (base, last) = (self.base?, self.last?);
162        let ms = i128::from(last.at) - i128::from(base.at);
163        if ms <= 0 {
164            return None;
165        }
166        let used = i128::from(last.limit.used) - i128::from(base.limit.used);
167        let earned = i128::from(last.limit.cap) - i128::from(base.limit.cap);
168        Some((used - earned, ms))
169    }
170
171    /// Requests a minute, net of what trading earned back: a bot that trades
172    /// as it quotes can be busy and still not run down.
173    #[must_use]
174    pub fn net_a_minute(&self) -> Option<i64> {
175        let (net, ms) = self.spent()?;
176        i64::try_from(net * 60_000 / ms).ok()
177    }
178
179    /// Milliseconds until nothing is left at the net pace; `None` when it is
180    /// not running down, or there is no pace yet.
181    #[must_use]
182    pub fn runs_out_ms(&self) -> Option<u64> {
183        let (net, ms) = self.spent()?;
184        if net <= 0 {
185            return None;
186        }
187        let left = i128::from(self.limit()?.remaining());
188        u64::try_from(left * ms / net).ok()
189    }
190}
191
192#[cfg(test)]
193#[path = "tests/limits.rs"]
194mod tests;