algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
rate_limit.hpp
1#pragma once
2
8#define ATL_TRANSPORT_LAYER_PRESENT 1
9
11
12#include <array>
13#include <chrono>
14#include <cstddef>
15#include <cstdint>
16#include <stdexcept>
17#include <string_view>
18
19namespace algotradelib {
20namespace network {
21
38
47
57inline constexpr int rate_limit_priority(RateLimitGroup group) noexcept {
58 switch (group) {
60 return 50;
62 return 40;
64 return 30;
66 return 20;
68 return 10;
69 }
70 return 0;
71}
72
78inline constexpr std::string_view rate_limit_group_name(RateLimitGroup group) noexcept {
79 switch (group) {
81 return "auth";
83 return "trading";
85 return "account";
87 return "market_data";
89 return "reference";
90 }
91 return "";
92}
93
94static_assert(
96 "all_rate_limit_groups must list every RateLimitGroup"
97);
98
100inline constexpr std::size_t rate_limit_group_count = all_rate_limit_groups.size();
101
107inline constexpr std::size_t rate_limit_group_index(RateLimitGroup group) noexcept {
108 return static_cast<std::size_t>(group);
109}
110
121 int burst_{8};
130};
131
138inline constexpr int usable_burst(const RateLimitBudget& budget, RateLimitGroup group) noexcept {
139 switch (group) {
140 // An order, and the token it needs, may use the reserve.
143 return budget.burst_;
147 break;
148 }
149 const int reserved = budget.burst_ - budget.trading_reserve_;
150 return reserved > 0 ? reserved : 1;
151}
152
163template <typename Clock = std::chrono::steady_clock>
165public:
167 using ClockType = Clock;
168
170 using TimePoint = typename Clock::time_point;
171
173 using Duration = std::chrono::nanoseconds;
174
182
188 explicit BasicRateLimitBucket(const RateLimitBudget& budget)
189 : BasicRateLimitBucket(budget.burst_, budget.per_second_) {}
190
197 void resize(int burst, int per_second) {
198 if (burst < 1) {
199 throw std::invalid_argument("rate limit burst must be at least one request");
200 }
201 if (per_second < 1) {
202 throw std::invalid_argument("rate limit rate must be at least one request per second");
203 }
204 burst_ = burst;
205 per_second_ = per_second;
206 interval_ = Duration(std::chrono::nanoseconds(std::chrono::seconds(1)).count() / per_second);
207 }
208
215 Duration delay_until(TimePoint now, int allowance) const {
216 if (!started_) {
217 return Duration::zero();
218 }
219 const auto earliest = schedule_ - tolerance(allowance);
220 if (now >= earliest) {
221 return Duration::zero();
222 }
223 return std::chrono::duration_cast<Duration>(earliest - now);
224 }
225
232 bool ready(TimePoint now, int allowance) const {
233 return delay_until(now, allowance) == Duration::zero();
234 }
235
243 bool try_acquire(TimePoint now, int allowance) {
244 if (!ready(now, allowance)) {
245 return false;
246 }
247 if (!started_) {
248 schedule_ = now + interval_;
249 started_ = true;
250 return true;
251 }
252 schedule_ = (schedule_ > now ? schedule_ : now) + interval_;
253 return true;
254 }
255
265 void cool_down(TimePoint now, Duration duration) {
266 if (duration < Duration::zero()) {
267 duration = Duration::zero();
268 }
269 const auto held = now + duration + tolerance(burst_);
270 if (!started_ || held > schedule_) {
271 schedule_ = held;
272 started_ = true;
273 }
274 }
275
284 void observe_limit(TimePoint now, int burst, int per_second, Duration refill) {
286 cool_down(now, refill > Duration::zero() ? refill : interval_);
287 }
288
295 int available(TimePoint now, int allowance) const {
296 if (!started_) {
297 return allowance;
298 }
299 const auto earliest = schedule_ - tolerance(allowance);
300 if (now < earliest) {
301 return 0;
302 }
303 const auto spare = std::chrono::duration_cast<Duration>(now - earliest);
304 const auto slots = spare / interval_ + 1;
305 return slots > allowance ? allowance : static_cast<int>(slots);
306 }
307
312 int burst() const noexcept { return burst_; }
313
318 int per_second() const noexcept { return per_second_; }
319
324 Duration interval() const noexcept { return interval_; }
325
326private:
327 // Returns how far ahead of now the schedule may run for `allowance`.
328 Duration tolerance(int allowance) const noexcept {
329 const int slots = allowance < 1 ? 1 : (allowance > burst_ ? burst_ : allowance);
330 return interval_ * (slots - 1);
331 }
332
333 int burst_{1};
334 int per_second_{1};
335 Duration interval_{std::chrono::seconds(1)};
336 TimePoint schedule_{};
337 bool started_{false};
338};
339
342
346 std::uint64_t admitted_{0};
348 std::uint64_t delayed_{0};
350 std::uint64_t shed_{0};
352 std::chrono::nanoseconds waited_{0};
353};
354
389
398public:
401
408 : budget_(budget), bucket_(budget) {
409 stats_.burst_ = budget.burst_;
411 }
412
419 return bucket_.delay_until(RateLimitBucket::ClockType::now(), usable_burst(budget_, group));
420 }
421
428 const auto now = RateLimitBucket::ClockType::now();
429 if (!bucket_.try_acquire(now, usable_burst(budget_, group))) {
430 return false;
431 }
432 ++stats_.group(group).admitted_;
433 stats_.available_ = bucket_.available(now, usable_burst(budget_, group));
434 return true;
435 }
436
443 auto& counters = stats_.group(group);
444 ++counters.delayed_;
445 counters.waited_ += waited;
446 }
447
452 void record_shed(RateLimitGroup group) { ++stats_.group(group).shed_; }
453
462 void hold(Duration duration) {
463 ++stats_.broker_rejections_;
464 bucket_.cool_down(RateLimitBucket::ClockType::now(), duration);
465 }
466
477 void shrink(int burst, int per_second, Duration retry) {
478 // The bucket validates first, so a refused figure leaves the reported budget the one in force.
479 bucket_.observe_limit(RateLimitBucket::ClockType::now(), burst, per_second, retry);
480 ++stats_.broker_rejections_;
481 apply_budget(burst, per_second);
482 }
483
488 const RateLimitBudget& budget() const noexcept { return budget_; }
489
494 const RateLimitStats& stats() const noexcept { return stats_; }
495
496private:
497 // Records a new allowance, keeping the reserve smaller than the burst.
498 void apply_budget(int burst, int per_second) {
499 budget_.burst_ = burst;
500 budget_.per_second_ = per_second;
501 if (budget_.trading_reserve_ >= burst) {
502 budget_.trading_reserve_ = burst > 1 ? burst - 1 : 0;
503 }
504 stats_.burst_ = burst;
505 stats_.per_second_ = per_second;
506 }
507
508 RateLimitBudget budget_;
509 RateLimitBucket bucket_;
510 RateLimitStats stats_{};
511};
512
513} // namespace network
514} // namespace algotradelib
A rate limiter (the generic cell rate algorithm) that computes when a request may go; it neither wait...
void observe_limit(TimePoint now, int burst, int per_second, Duration refill)
Adopts a burst and rate the broker reported, then holds requests until it refills.
int available(TimePoint now, int allowance) const
Returns how many requests could go back to back now.
Duration interval() const noexcept
Returns the spacing of the sustained rate.
std::chrono::nanoseconds Duration
The duration type of all results.
void cool_down(TimePoint now, Duration duration)
Holds every request for at least a duration (for a broker refusal that is not this client's pacing fa...
typename Clock::time_point TimePoint
The clock's instant type.
int burst() const noexcept
Returns the burst.
BasicRateLimitBucket(const RateLimitBudget &budget)
Constructs a bucket from a budget's burst and rate.
BasicRateLimitBucket(int burst, int per_second)
Constructs a bucket.
Duration delay_until(TimePoint now, int allowance) const
Returns how long a request must wait.
void resize(int burst, int per_second)
Changes the burst and rate, keeping the current schedule.
bool try_acquire(TimePoint now, int allowance)
Admits one request if it may go now.
int per_second() const noexcept
Returns the sustained rate.
bool ready(TimePoint now, int allowance) const
Tests whether a request may go now.
Duration delay_until(RateLimitGroup group) const
Returns how long a request of a class must wait.
const RateLimitBudget & budget() const noexcept
Returns the budget in force.
bool acquire(RateLimitGroup group)
Admits one request of a class if it may go now, and counts it.
void shrink(int burst, int per_second, Duration retry)
Counts a broker refusal, adopts the burst and rate the broker reported, and holds.
RateLimitController(RateLimitBudget budget={})
Constructs a controller.
RateLimitBucket::Duration Duration
The duration type.
const RateLimitStats & stats() const noexcept
Returns the counters.
void record_shed(RateLimitGroup group)
Records that a request was refused unsent.
void record_delay(RateLimitGroup group, Duration waited)
Records that a request waited before admission.
void hold(Duration duration)
Counts a broker refusal and holds every request for a duration, keeping the budget.
A compile-time check that a list names every value of an enum, and a parser over the list.
HTTP and WebSocket transport, OAuth tokens, and request pacing.
RateLimitGroup
The traffic class of a request, for rate limiting and queue priority. The broker adapter that builds ...
@ MARKET_DATA
Quotes, bars, order book and trades.
@ ACCOUNT
Holdings, working orders, order history and buying power.
@ TRADING
Order placement, amendment and cancellation.
@ REFERENCE
Instrument reference data and trading calendars.
@ AUTH
Access-token issue and refresh.
constexpr int rate_limit_priority(RateLimitGroup group) noexcept
Returns a traffic class's queue priority.
constexpr int usable_burst(const RateLimitBudget &budget, RateLimitGroup group) noexcept
Returns how much of the burst a traffic class may use.
constexpr std::array< RateLimitGroup, 5 > all_rate_limit_groups
Every RateLimitGroup, in declaration order.
constexpr std::size_t rate_limit_group_index(RateLimitGroup group) noexcept
Returns a traffic class's index, for per-class arrays.
constexpr std::size_t rate_limit_group_count
The number of traffic classes.
BasicRateLimitBucket<> RateLimitBucket
A rate-limit bucket on the steady clock.
constexpr std::string_view rate_limit_group_name(RateLimitGroup group) noexcept
Returns a lowercase name for a traffic class, such as "market_data", for logs.
The library: broker-neutral types (core), and the modules below.
constexpr bool lists_every_value(const std::array< Enum, N > &values, IsEnumerator is_enumerator) noexcept
Tests whether a list holds every value of an enum exactly once, in any order.
The request rate a connection may use.
int trading_reserve_
The part of burst_ only AUTH and TRADING requests may use, so polling cannot use up the capacity an o...
int per_second_
The sustained rate, in requests per second; at least 1.
int burst_
How many requests may be sent back to back after an idle period; at least 1.
A rate limiter's counters for one traffic class.
std::uint64_t delayed_
Admitted requests that had to wait first (on a polling class, often the trading reserve at work).
std::chrono::nanoseconds waited_
Total time admitted requests waited.
std::uint64_t admitted_
Requests admitted.
std::uint64_t shed_
Requests refused locally: the queue was full, or the wait would have exceeded the timeout.
A rate limiter's counters per traffic class, its current budget, and the broker's refusals (from Rate...
std::uint64_t broker_rejections_
How many rate-limit refusals the broker sent; if it keeps rising, lower the budget.
RateLimitGroupStats groups_[rate_limit_group_count]
The counters per traffic class; read them with group().
int per_second_
The sustained rate in force.
const RateLimitGroupStats & group(RateLimitGroup value) const
Returns the counters for one traffic class.
int available_
How much of the burst was unspent just after the last admitted request (for that request's class); no...
RateLimitGroupStats & group(RateLimitGroup value)
Returns the counters for one traffic class.