Skip to main content

brokapi_bybit/
limits.rs

1//! Loads `limits.toml` (section `[bybit]`) through `BrokerLimits::from_toml_section`.
2//!
3//! The exchange publishes a limit per second and endpoint for each user (10 a second for placing, amending and
4//! cancelling, 50 for the reads) and one for each IP address (600 requests in 5 seconds). The bundled values are the
5//! published ones. One `BrokerLimiter` per login: give every connector of one Bybit login the same
6//! `Arc<BrokerLimiter>`.
7
8use brokapi_core::ConfigError;
9use brokapi_runtime::BrokerLimits;
10
11/// The text of the bundled limits file `limits.toml`, compiled into the crate.
12///
13/// Parse it with [`limits_from_toml_str`], or take the result directly from [`default_limits`].
14pub const DEFAULT_LIMITS_TOML: &str = include_str!("../limits.toml");
15
16/// Reads the `[bybit]` section of a limits file given as text.
17///
18/// Other sections of the file (other brokers) are ignored.
19///
20/// # Errors
21///
22/// [`brokapi_core::ConfigError::Message`] when the text is not valid TOML, when there is no `[bybit]` section, or
23/// when the section is not a valid set of limits (see [`BrokerLimits::from_toml_section`]).
24///
25/// # Examples
26///
27/// ```
28/// use brokapi_bybit::limits::{limits_from_toml_str, DEFAULT_LIMITS_TOML};
29///
30/// assert!(limits_from_toml_str(DEFAULT_LIMITS_TOML).is_ok());
31/// // A file for another broker has no [bybit] section.
32/// assert!(limits_from_toml_str("[tbank.buckets.global]\nrate = 1").is_err());
33/// ```
34pub fn limits_from_toml_str(raw: &str) -> Result<BrokerLimits, ConfigError> {
35    BrokerLimits::from_toml_section(raw, "bybit")
36}
37
38/// Reads the `[bybit]` section of a limits file on disk, to tune the limits without recompiling.
39///
40/// # Errors
41///
42/// [`brokapi_core::ConfigError::Message`] when the file cannot be read (the message names the path), and for
43/// every reason [`limits_from_toml_str`] lists.
44///
45/// # Examples
46///
47/// ```
48/// use brokapi_bybit::limits::limits_from_toml_path;
49///
50/// let err = limits_from_toml_path("/definitely/not/here.toml").unwrap_err();
51/// assert!(err.to_string().contains("/definitely/not/here.toml"));
52/// ```
53pub fn limits_from_toml_path(path: &str) -> Result<BrokerLimits, ConfigError> {
54    let raw = std::fs::read_to_string(path)
55        .map_err(|e| ConfigError::Message(format!("read {path}: {e}")))?;
56    limits_from_toml_str(&raw)
57}
58
59/// The bundled Bybit limits, parsed.
60///
61/// A method with no route of its own spends the default route `"*"`, so no call is free. Placing goes through a
62/// soft bucket of its own that cancel does not pass.
63///
64/// # Panics
65///
66/// Never in practice: the bundled file is checked by a test of this crate.
67///
68/// # Examples
69///
70/// ```
71/// use brokapi_bybit::limits::default_limits;
72/// use brokapi_runtime::BrokerLimiter;
73/// use std::time::Instant;
74///
75/// let limiter = BrokerLimiter::new(&default_limits());
76/// assert!(limiter.acquire("PlaceOrder", Instant::now()).is_ok());
77/// ```
78pub fn default_limits() -> BrokerLimits {
79    limits_from_toml_str(DEFAULT_LIMITS_TOML).expect("bundled bybit limits.toml is valid")
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn bundled_limits_parse_and_every_route_points_to_a_bucket() {
88        let l = default_limits();
89        for method in [
90            "PlaceOrder",
91            "CancelOrder",
92            "CancelAll",
93            "ReplaceOrder",
94            "PlaceStopOrder",
95            "CancelStopOrder",
96            "GetOrder",
97            "GetOrders",
98            "GetOrderHistory",
99            "GetExecutions",
100            "GetPositions",
101            "GetWallet",
102            "GetOrderBook",
103            "GetTickers",
104            "GetLastTrades",
105            "GetCandles",
106            "GetInstruments",
107            "GetServerTime",
108            "Reconcile",
109        ] {
110            let buckets = l.buckets_for(method);
111            assert!(!buckets.is_empty(), "{method} is not routed");
112            for b in buckets {
113                assert!(l.buckets.contains_key(&b), "{method}: unknown bucket {b}");
114            }
115        }
116        assert!(!l.buckets_for("SomethingNew").is_empty());
117        // Every route starts with the address limit, and placing has a bucket of its own that a cancel does not use.
118        assert_eq!(l.route["PlaceOrder"], ["ip", "create"]);
119        assert_eq!(l.route["CancelOrder"], ["ip", "cancel"]);
120        // The documented limit of orders is 10 a second per endpoint; the reads have 50.
121        assert_eq!(l.buckets["create"].rate, 10);
122        assert_eq!(l.buckets["trade_read"].rate, 50);
123    }
124
125    #[test]
126    fn a_file_with_no_bybit_section_is_refused() {
127        assert!(limits_from_toml_str("[tbank.buckets.global]\nrate=1").is_err());
128    }
129}