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}