#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TradingBucket {
Order,
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Tier {
#[default]
Standard,
Copper,
Bronze,
Silver,
Gold,
Platinum,
Diamond,
Elite,
}
impl Tier {
pub fn rate(self, bucket: TradingBucket) -> u32 {
let (order_rate, _, cancel_rate, _) = self.allowances();
match bucket {
TradingBucket::Order => order_rate,
TradingBucket::Cancel => cancel_rate,
}
}
pub fn burst(self, bucket: TradingBucket) -> u32 {
let (_, order_burst, _, cancel_burst) = self.allowances();
match bucket {
TradingBucket::Order => order_burst,
TradingBucket::Cancel => cancel_burst,
}
}
fn allowances(self) -> (u32, u32, u32, u32) {
match self {
Tier::Standard => (40, 60, 80, 120),
Tier::Copper => (60, 90, 120, 180),
Tier::Bronze => (80, 120, 160, 240),
Tier::Silver => (200, 300, 400, 600),
Tier::Gold => (400, 600, 800, 1_200),
Tier::Platinum => (450, 675, 900, 1_350),
Tier::Diamond => (525, 787, 1_050, 1_575),
Tier::Elite => (600, 900, 1_200, 1_800),
}
}
pub fn from_header(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"standard" => Some(Tier::Standard),
"copper" => Some(Tier::Copper),
"bronze" => Some(Tier::Bronze),
"silver" => Some(Tier::Silver),
"gold" => Some(Tier::Gold),
"platinum" => Some(Tier::Platinum),
"diamond" => Some(Tier::Diamond),
"elite" => Some(Tier::Elite),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TradingRequest {
PostOrder,
PostOrders {
count: u32,
},
CancelOrder,
CancelOrders {
count: u32,
},
CancelAll,
CancelMarketOrders,
}
impl TradingRequest {
pub fn bucket(self) -> TradingBucket {
match self {
TradingRequest::PostOrder | TradingRequest::PostOrders { .. } => TradingBucket::Order,
TradingRequest::CancelOrder
| TradingRequest::CancelOrders { .. }
| TradingRequest::CancelAll
| TradingRequest::CancelMarketOrders => TradingBucket::Cancel,
}
}
pub fn cost(self) -> u32 {
match self {
TradingRequest::PostOrder | TradingRequest::CancelOrder => 1,
TradingRequest::PostOrders { count } | TradingRequest::CancelOrders { count } => count,
TradingRequest::CancelAll | TradingRequest::CancelMarketOrders => 1,
}
}
pub fn cost_is_exact(self) -> bool {
!matches!(
self,
TradingRequest::CancelAll | TradingRequest::CancelMarketOrders
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RateLimitStatus {
pub remaining: Option<u32>,
pub reset: Option<u64>,
pub tier: Option<Tier>,
pub warning: bool,
}
impl RateLimitStatus {
pub fn from_headers(headers: &reqwest::header::HeaderMap) -> Self {
let get = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
Self {
remaining: get("poly-ratelimit-remaining").and_then(|v| v.trim().parse().ok()),
reset: get("poly-ratelimit-reset").and_then(|v| v.trim().parse().ok()),
tier: get("poly-ratelimit-tier").and_then(Tier::from_header),
warning: get("poly-ratelimit-warning")
.is_some_and(|v| v.trim().eq_ignore_ascii_case("true")),
}
}
pub fn is_empty(&self) -> bool {
self.remaining.is_none() && self.reset.is_none() && self.tier.is_none() && !self.warning
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"batch costs {cost} tokens but the {bucket:?} bucket at tier {tier:?} holds at most \
{capacity}; this can never succeed — split it into batches of {capacity} or fewer"
)]
pub struct BurstCapacityExceeded {
pub cost: u32,
pub capacity: u32,
pub tier: Tier,
pub bucket: TradingBucket,
}
type DirectLimiter = governor::RateLimiter<
governor::state::NotKeyed,
governor::state::InMemoryState,
governor::clock::DefaultClock,
>;
struct Buckets {
tier: Tier,
order: std::sync::Arc<DirectLimiter>,
cancel: std::sync::Arc<DirectLimiter>,
}
impl Buckets {
fn for_tier(tier: Tier) -> Self {
let build = |bucket: TradingBucket| {
let rate = tier.rate(bucket).max(1);
let burst = tier.burst(bucket).max(1);
let quota = governor::Quota::with_period(std::time::Duration::from_secs(1) / rate)
.expect("per-token interval is non-zero")
.allow_burst(std::num::NonZeroU32::new(burst).expect("burst is non-zero"));
std::sync::Arc::new(DirectLimiter::direct(quota))
};
Self {
tier,
order: build(TradingBucket::Order),
cancel: build(TradingBucket::Cancel),
}
}
}
#[derive(Clone)]
pub struct SignerLimiter {
inner: std::sync::Arc<SignerLimiterInner>,
}
struct SignerLimiterInner {
buckets: std::sync::RwLock<Buckets>,
status: std::sync::RwLock<RateLimitStatus>,
}
impl std::fmt::Debug for SignerLimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SignerLimiter")
.field("tier", &self.tier())
.finish()
}
}
impl Default for SignerLimiter {
fn default() -> Self {
Self::new()
}
}
impl SignerLimiter {
pub fn new() -> Self {
Self {
inner: std::sync::Arc::new(SignerLimiterInner {
buckets: std::sync::RwLock::new(Buckets::for_tier(Tier::default())),
status: std::sync::RwLock::new(RateLimitStatus::default()),
}),
}
}
pub fn tier(&self) -> Tier {
self.inner
.buckets
.read()
.expect("lock is never poisoned")
.tier
}
pub fn last_status(&self) -> RateLimitStatus {
*self.inner.status.read().expect("lock is never poisoned")
}
pub fn observe(&self, headers: &reqwest::header::HeaderMap) {
let status = RateLimitStatus::from_headers(headers);
if status.is_empty() {
return;
}
*self.inner.status.write().expect("lock is never poisoned") = status;
if let Some(tier) = status.tier {
let mut buckets = self.inner.buckets.write().expect("lock is never poisoned");
if buckets.tier != tier {
tracing::debug!("adopting rate limit tier {tier:?} (was {:?})", buckets.tier);
*buckets = Buckets::for_tier(tier);
}
}
}
pub async fn acquire(&self, request: TradingRequest) -> Result<(), BurstCapacityExceeded> {
let bucket = request.bucket();
let (tier, limiter) = {
let buckets = self.inner.buckets.read().expect("lock is never poisoned");
let limiter = match bucket {
TradingBucket::Order => buckets.order.clone(),
TradingBucket::Cancel => buckets.cancel.clone(),
};
(buckets.tier, limiter)
};
let cost = request.cost().max(1);
let n = std::num::NonZeroU32::new(cost).expect("cost floor is 1");
limiter
.until_n_ready(n)
.await
.map_err(|_| BurstCapacityExceeded {
cost,
capacity: tier.burst(bucket),
tier,
bucket,
})
}
}
#[cfg(test)]
mod limiter_tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use std::time::Duration;
fn tier_header(tier: &str) -> HeaderMap {
let mut map = HeaderMap::new();
map.insert(
HeaderName::from_static("poly-ratelimit-tier"),
HeaderValue::from_str(tier).unwrap(),
);
map
}
#[test]
fn starts_at_the_tightest_tier() {
assert_eq!(SignerLimiter::new().tier(), Tier::Standard);
}
#[test]
fn observing_a_tier_header_adopts_it() {
let limiter = SignerLimiter::new();
limiter.observe(&tier_header("gold"));
assert_eq!(limiter.tier(), Tier::Gold);
assert_eq!(limiter.last_status().tier, Some(Tier::Gold));
}
#[test]
fn an_unrecognised_tier_leaves_the_current_one_in_force() {
let limiter = SignerLimiter::new();
limiter.observe(&tier_header("silver"));
limiter.observe(&tier_header("titanium"));
assert_eq!(limiter.tier(), Tier::Silver);
}
#[test]
fn a_response_without_the_headers_does_not_clear_telemetry() {
let limiter = SignerLimiter::new();
limiter.observe(&tier_header("silver"));
limiter.observe(&HeaderMap::new());
assert_eq!(limiter.tier(), Tier::Silver);
assert_eq!(limiter.last_status().tier, Some(Tier::Silver));
}
#[tokio::test]
async fn an_over_capacity_batch_is_rejected_immediately_not_queued() {
let limiter = SignerLimiter::new();
let request = TradingRequest::CancelOrders { count: 2_000 };
let result = tokio::time::timeout(Duration::from_millis(100), limiter.acquire(request))
.await
.expect("must not hang waiting for capacity that can never exist");
let err = result.expect_err("2,000 tokens exceeds Standard's 120 cancel burst");
assert_eq!(err.cost, 2_000);
assert_eq!(err.capacity, 120);
assert_eq!(err.bucket, TradingBucket::Cancel);
}
#[tokio::test]
async fn a_batch_within_capacity_is_admitted() {
let limiter = SignerLimiter::new();
limiter
.acquire(TradingRequest::CancelOrders { count: 100 })
.await
.expect("100 fits Standard's 120 cancel burst");
}
#[tokio::test]
async fn adopting_a_higher_tier_admits_a_batch_that_was_impossible() {
let limiter = SignerLimiter::new();
let batch = TradingRequest::CancelOrders { count: 500 };
assert!(limiter.acquire(batch).await.is_err());
limiter.observe(&tier_header("gold"));
limiter
.acquire(batch)
.await
.expect("500 fits Gold's 1,200 cancel burst");
}
#[tokio::test]
async fn the_order_and_cancel_buckets_are_independent() {
let limiter = SignerLimiter::new();
limiter
.acquire(TradingRequest::PostOrders { count: 60 })
.await
.expect("60 fills Standard's order burst exactly");
let start = std::time::Instant::now();
limiter
.acquire(TradingRequest::CancelOrder)
.await
.expect("cancel bucket is untouched");
assert!(
start.elapsed() < Duration::from_millis(25),
"cancelling was throttled by order placement"
);
}
#[tokio::test]
async fn batch_cost_is_charged_in_full_not_as_one_request() {
let limiter = SignerLimiter::new();
limiter
.acquire(TradingRequest::PostOrders { count: 60 })
.await
.unwrap();
let start = std::time::Instant::now();
limiter.acquire(TradingRequest::PostOrder).await.unwrap();
assert!(
start.elapsed() >= Duration::from_millis(10),
"a 60-order batch was charged as a single token"
);
}
}
#[cfg(test)]
mod status_tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for (k, v) in pairs {
map.insert(
HeaderName::from_bytes(k.as_bytes()).unwrap(),
HeaderValue::from_str(v).unwrap(),
);
}
map
}
#[test]
fn reads_the_full_header_family() {
let status = RateLimitStatus::from_headers(&headers(&[
("poly-ratelimit-remaining", "57"),
("poly-ratelimit-reset", "1767225660"),
("poly-ratelimit-tier", "silver"),
("poly-ratelimit-warning", "true"),
]));
assert_eq!(status.remaining, Some(57));
assert_eq!(status.reset, Some(1_767_225_660));
assert_eq!(status.tier, Some(Tier::Silver));
assert!(status.warning);
assert!(!status.is_empty());
}
#[test]
fn header_names_are_matched_case_insensitively() {
let status = RateLimitStatus::from_headers(&headers(&[
("Poly-RateLimit-Tier", "GOLD"),
("POLY-RATELIMIT-REMAINING", "3"),
]));
assert_eq!(status.tier, Some(Tier::Gold));
assert_eq!(status.remaining, Some(3));
}
#[test]
fn absent_headers_yield_an_empty_status_not_an_error() {
let status = RateLimitStatus::from_headers(&HeaderMap::new());
assert!(status.is_empty());
assert_eq!(status, RateLimitStatus::default());
}
#[test]
fn malformed_values_are_dropped_rather_than_guessed() {
let status = RateLimitStatus::from_headers(&headers(&[
("poly-ratelimit-remaining", "not-a-number"),
("poly-ratelimit-reset", ""),
("poly-ratelimit-tier", "titanium"),
("poly-ratelimit-warning", "false"),
]));
assert_eq!(status.remaining, None);
assert_eq!(status.reset, None);
assert_eq!(status.tier, None);
assert!(!status.warning);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn published() -> Vec<(Tier, u32, u32, u32, u32)> {
vec![
(Tier::Standard, 40, 60, 80, 120),
(Tier::Copper, 60, 90, 120, 180),
(Tier::Bronze, 80, 120, 160, 240),
(Tier::Silver, 200, 300, 400, 600),
(Tier::Gold, 400, 600, 800, 1_200),
(Tier::Platinum, 450, 675, 900, 1_350),
(Tier::Diamond, 525, 787, 1_050, 1_575),
(Tier::Elite, 600, 900, 1_200, 1_800),
]
}
#[test]
fn every_tier_matches_the_published_table() {
for (tier, o_rate, o_burst, c_rate, c_burst) in published() {
assert_eq!(
tier.rate(TradingBucket::Order),
o_rate,
"{tier:?} order rate"
);
assert_eq!(
tier.burst(TradingBucket::Order),
o_burst,
"{tier:?} order burst"
);
assert_eq!(
tier.rate(TradingBucket::Cancel),
c_rate,
"{tier:?} cancel rate"
);
assert_eq!(
tier.burst(TradingBucket::Cancel),
c_burst,
"{tier:?} cancel burst"
);
}
}
#[test]
fn the_default_tier_is_the_tightest_one() {
let default = Tier::default();
for (tier, ..) in published() {
assert!(
default.rate(TradingBucket::Order) <= tier.rate(TradingBucket::Order),
"default tier {default:?} is looser than {tier:?}"
);
assert!(
default.burst(TradingBucket::Cancel) <= tier.burst(TradingBucket::Cancel),
"default tier {default:?} bursts higher than {tier:?}"
);
}
}
#[test]
fn tier_headers_parse_case_insensitively() {
assert_eq!(Tier::from_header("standard"), Some(Tier::Standard));
assert_eq!(Tier::from_header("Silver"), Some(Tier::Silver));
assert_eq!(Tier::from_header("ELITE"), Some(Tier::Elite));
}
#[test]
fn an_unknown_tier_header_is_not_guessed() {
assert_eq!(Tier::from_header("titanium"), None);
assert_eq!(Tier::from_header(""), None);
}
#[test]
fn batch_costs_scale_with_the_payload() {
assert_eq!(TradingRequest::PostOrder.cost(), 1);
assert_eq!(TradingRequest::PostOrders { count: 40 }.cost(), 40);
assert_eq!(TradingRequest::CancelOrder.cost(), 1);
assert_eq!(TradingRequest::CancelOrders { count: 250 }.cost(), 250);
}
#[test]
fn requests_draw_from_the_right_bucket() {
assert_eq!(TradingRequest::PostOrder.bucket(), TradingBucket::Order);
assert_eq!(
TradingRequest::PostOrders { count: 2 }.bucket(),
TradingBucket::Order
);
assert_eq!(TradingRequest::CancelOrder.bucket(), TradingBucket::Cancel);
assert_eq!(
TradingRequest::CancelOrders { count: 2 }.bucket(),
TradingBucket::Cancel
);
assert_eq!(TradingRequest::CancelAll.bucket(), TradingBucket::Cancel);
assert_eq!(
TradingRequest::CancelMarketOrders.bucket(),
TradingBucket::Cancel
);
}
#[test]
fn cancel_all_reports_a_floor_cost_and_says_so() {
assert_eq!(TradingRequest::CancelAll.cost(), 1);
assert!(!TradingRequest::CancelAll.cost_is_exact());
assert_eq!(TradingRequest::CancelMarketOrders.cost(), 1);
assert!(!TradingRequest::CancelMarketOrders.cost_is_exact());
for exact in [
TradingRequest::PostOrder,
TradingRequest::PostOrders { count: 3 },
TradingRequest::CancelOrder,
TradingRequest::CancelOrders { count: 3 },
] {
assert!(exact.cost_is_exact(), "{exact:?} cost is computable");
}
}
#[test]
fn a_batch_larger_than_elite_burst_is_impossible_on_every_tier() {
let batch = TradingRequest::CancelOrders { count: 2_000 };
for (tier, ..) in published() {
assert!(
batch.cost() > tier.burst(batch.bucket()),
"{tier:?} could absorb a 2,000-ID batch — check the published table"
);
}
}
}