use std::num::NonZeroU32;
use nautilus_network::ratelimiter::quota::Quota;
pub const DERIVE_MATCHING_RATE_KEY: &str = "derive:matching";
pub const DERIVE_NON_MATCHING_RATE_KEY: &str = "derive:non-matching";
pub const DERIVE_CANCEL_ALL_RATE_KEY: &str = "derive:cancel-all";
pub const DERIVE_CANCEL_BY_LABEL_RATE_KEY: &str = "derive:cancel-by-label";
pub const DERIVE_DEFAULT_MATCHING_TPS: u32 = 1;
pub const DERIVE_NON_MATCHING_TPS: u32 = 10;
pub const DERIVE_WEBSOCKET_NON_MATCHING_TPS: u32 = 5;
pub const DERIVE_CANCEL_ALL_TPS: u32 = 1;
pub const DERIVE_CANCEL_BY_LABEL_TPS: u32 = 10;
pub const DERIVE_RATE_BURST_MULTIPLIER: u32 = 5;
#[must_use]
pub fn matching_quota(max_requests_per_second: Option<u32>) -> Quota {
let tps = max_requests_per_second
.filter(|&v| v > 0)
.unwrap_or(DERIVE_DEFAULT_MATCHING_TPS);
quota_with_burst(tps)
}
#[must_use]
pub fn non_matching_quota() -> Quota {
quota_with_burst(DERIVE_NON_MATCHING_TPS)
}
#[must_use]
pub fn websocket_non_matching_quota() -> Quota {
quota_with_burst(DERIVE_WEBSOCKET_NON_MATCHING_TPS)
}
#[must_use]
pub fn cancel_all_quota() -> Quota {
quota_with_burst(DERIVE_CANCEL_ALL_TPS)
}
#[must_use]
pub fn cancel_by_label_quota() -> Quota {
quota_with_burst(DERIVE_CANCEL_BY_LABEL_TPS)
}
#[must_use]
pub(crate) fn rate_limit_key_for_method(method: &str) -> &'static str {
match method.trim_start_matches('/') {
"private/order"
| "private/trigger_order"
| "private/replace"
| "private/cancel"
| "private/cancel_trigger_order" => DERIVE_MATCHING_RATE_KEY,
"private/cancel_all" => DERIVE_CANCEL_ALL_RATE_KEY,
"private/cancel_by_label" => DERIVE_CANCEL_BY_LABEL_RATE_KEY,
_ => DERIVE_NON_MATCHING_RATE_KEY,
}
}
fn quota_with_burst(tps: u32) -> Quota {
let rate = NonZeroU32::new(tps).expect("tps must be non-zero");
let burst = NonZeroU32::new(tps.saturating_mul(DERIVE_RATE_BURST_MULTIPLIER))
.expect("burst must be non-zero");
Quota::per_second(rate)
.expect("per-second quota replenish interval must be non-zero")
.allow_burst(burst)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use rstest::rstest;
use super::*;
#[rstest]
fn test_non_matching_quota_is_ten_per_second_with_five_second_burst() {
let quota = non_matching_quota();
assert_eq!(quota.burst_size().get(), 50);
assert_eq!(quota.replenish_interval(), Duration::from_millis(100));
}
#[rstest]
fn test_websocket_non_matching_quota_is_five_per_second_with_five_second_burst() {
let quota = websocket_non_matching_quota();
assert_eq!(quota.burst_size().get(), 25);
assert_eq!(quota.replenish_interval(), Duration::from_millis(200));
}
#[rstest]
fn test_custom_cancel_quotas_match_venue_limits() {
let cancel_all = cancel_all_quota();
let cancel_by_label = cancel_by_label_quota();
assert_eq!(cancel_all.burst_size().get(), 5);
assert_eq!(cancel_all.replenish_interval(), Duration::from_secs(1));
assert_eq!(cancel_by_label.burst_size().get(), 50);
assert_eq!(
cancel_by_label.replenish_interval(),
Duration::from_millis(100),
);
}
#[rstest]
fn test_matching_quota_defaults_to_trader_tier() {
let quota = matching_quota(None);
assert_eq!(quota.burst_size().get(), 5);
assert_eq!(quota.replenish_interval(), Duration::from_secs(1));
}
#[rstest]
fn test_matching_quota_treats_zero_as_unset() {
assert_eq!(matching_quota(Some(0)).burst_size().get(), 5);
}
#[rstest]
fn test_matching_quota_honors_market_maker_override() {
let quota = matching_quota(Some(500));
assert_eq!(quota.burst_size().get(), 2500);
assert_eq!(quota.replenish_interval(), Duration::from_millis(2));
}
#[rstest]
#[case("private/order", DERIVE_MATCHING_RATE_KEY)]
#[case("/private/order", DERIVE_MATCHING_RATE_KEY)]
#[case("private/trigger_order", DERIVE_MATCHING_RATE_KEY)]
#[case("private/replace", DERIVE_MATCHING_RATE_KEY)]
#[case("private/cancel", DERIVE_MATCHING_RATE_KEY)]
#[case("private/cancel_trigger_order", DERIVE_MATCHING_RATE_KEY)]
#[case("private/cancel_all", DERIVE_CANCEL_ALL_RATE_KEY)]
#[case("private/cancel_by_label", DERIVE_CANCEL_BY_LABEL_RATE_KEY)]
#[case("private/get_subaccount", DERIVE_NON_MATCHING_RATE_KEY)]
#[case("private/get_open_orders", DERIVE_NON_MATCHING_RATE_KEY)]
#[case("public/get_instruments", DERIVE_NON_MATCHING_RATE_KEY)]
#[case("public/login", DERIVE_NON_MATCHING_RATE_KEY)]
#[case("subscribe", DERIVE_NON_MATCHING_RATE_KEY)]
fn test_rate_limit_key_for_method(#[case] method: &str, #[case] expected: &str) {
assert_eq!(rate_limit_key_for_method(method), expected);
}
}