futu_auth/limits/runtime/
attempted_quota.rs1use super::*;
7use chrono::{DateTime, Local, Utc};
8
9impl RuntimeCounters {
10 #[must_use]
20 pub fn check_and_commit(
21 &self,
22 key_id: &str,
23 limits: &(impl LimitPolicy + ?Sized),
24 ctx: &CheckCtx,
25 now: DateTime<Utc>,
26 ) -> LimitOutcome {
27 if let (Some(allowed), Some(id)) = (limits.allowed_acc_ids(), ctx.acc_id)
32 && !allowed.is_empty()
33 && !allowed.contains(&id)
34 {
35 return LimitOutcome::WhitelistReject(format!(
36 "acc_id {id} not in allowed list {allowed:?}"
37 ));
38 }
39
40 if let Some(markets) = limits.allowed_markets()
42 && !markets.is_empty()
43 && !ctx.market.is_empty()
44 && !markets.contains(&ctx.market)
45 {
46 return LimitOutcome::WhitelistReject(format!(
47 "market {:?} not in allowed list {:?}",
48 ctx.market, markets
49 ));
50 }
51
52 if let Some(symbols) = limits.allowed_symbols()
54 && !symbols.is_empty()
55 && !ctx.symbol.is_empty()
56 && !symbols.contains(&ctx.symbol)
57 {
58 return LimitOutcome::WhitelistReject(format!(
59 "symbol {:?} not in allowed list",
60 ctx.symbol
61 ));
62 }
63
64 if let (Some(allowed), Some(side)) = (limits.allowed_trd_sides(), &ctx.trd_side)
66 && !allowed.is_empty()
67 && !allowed.contains(side)
68 {
69 return LimitOutcome::WhitelistReject(format!(
70 "trd_side {side:?} not in allowed list {allowed:?}"
71 ));
72 }
73
74 if let Some(spec) = limits.hours_window() {
76 match parse_window(spec) {
77 Ok((start, end)) => {
78 let now_local = now.with_timezone(&Local).time();
79 if !in_window(now_local, start, end) {
80 return LimitOutcome::ThroughputReject(format!(
81 "outside hours window {spec} (now={})",
82 now_local.format("%H:%M")
83 ));
84 }
85 }
86 Err(e) => {
87 return LimitOutcome::ThroughputReject(format!(
88 "invalid hours_window {spec:?}: {e}"
89 ));
90 }
91 }
92 }
93
94 if let Some(value) = ctx.order_value {
100 if let Err(reason) = validate_order_value(value) {
101 return LimitOutcome::ValueReject(format!(
102 "order value invalid ({reason:?}): {value}"
103 ));
104 }
105 if let Some(cap) = limits.max_order_value()
106 && value > cap + f64::EPSILON
107 {
108 return LimitOutcome::ValueReject(format!(
109 "order value {value:.2} exceeds per-order cap {cap:.2}"
110 ));
111 }
112 }
113
114 if let Some(max) = limits.max_orders_per_minute() {
116 let window = self.rates.entry(key_id.to_string()).or_default();
117 if let Err(e) = window.try_record(now, max) {
118 return LimitOutcome::ThroughputReject(e);
119 }
120 }
121
122 if !ctx.mutation_no_exposure
128 && let (Some(value), Some(_)) = (ctx.order_value, limits.max_daily_value())
129 {
130 let daily_currency = ctx.daily_currency();
131 let today = now.date_naive();
132 let counter = self
133 .counters
134 .entry(key_id.to_string())
135 .or_insert_with(|| DailyCounter::new(today));
136 match counter.try_add(
137 daily_currency.as_deref(),
138 value,
139 limits.max_daily_value(),
140 today,
141 ) {
142 Ok(_) => {}
143 Err(DailyAddError::OverCap(msg)) => return LimitOutcome::ThroughputReject(msg),
144 Err(DailyAddError::Invalid(reason)) => {
145 return LimitOutcome::ValueReject(format!(
148 "order value invalid ({reason:?}): {value}"
149 ));
150 }
151 Err(DailyAddError::StaleDay {
152 guard_day,
153 current_day,
154 }) => return stale_day_outcome(guard_day, current_day),
155 Err(DailyAddError::Underflow {
156 currency,
157 current,
158 release,
159 }) => {
160 return LimitOutcome::ValueReject(format!(
161 "daily receipt release underflow for {currency}: current={current:.2}, release={release:.2}"
162 ));
163 }
164 }
165 }
166
167 LimitOutcome::Allow
168 }
169
170 #[must_use]
182 pub fn check_full_skip_rate(
183 &self,
184 key_id: &str,
185 limits: &(impl LimitPolicy + ?Sized),
186 ctx: &CheckCtx,
187 now: DateTime<Utc>,
188 ) -> LimitOutcome {
189 if let (Some(allowed), Some(id)) = (limits.allowed_acc_ids(), ctx.acc_id)
191 && !allowed.is_empty()
192 && !allowed.contains(&id)
193 {
194 return LimitOutcome::WhitelistReject(format!(
195 "acc_id {id} not in allowed list {allowed:?}"
196 ));
197 }
198
199 if let Some(markets) = limits.allowed_markets()
201 && !markets.is_empty()
202 && !ctx.market.is_empty()
203 && !markets.contains(&ctx.market)
204 {
205 return LimitOutcome::WhitelistReject(format!(
206 "market {:?} not in allowed list {:?}",
207 ctx.market, markets
208 ));
209 }
210
211 if let Some(symbols) = limits.allowed_symbols()
213 && !symbols.is_empty()
214 && !ctx.symbol.is_empty()
215 && !symbols.contains(&ctx.symbol)
216 {
217 return LimitOutcome::WhitelistReject(format!(
218 "symbol {:?} not in allowed list",
219 ctx.symbol
220 ));
221 }
222
223 if let (Some(allowed), Some(side)) = (limits.allowed_trd_sides(), &ctx.trd_side)
225 && !allowed.is_empty()
226 && !allowed.contains(side)
227 {
228 return LimitOutcome::WhitelistReject(format!(
229 "trd_side {side:?} not in allowed list {allowed:?}"
230 ));
231 }
232
233 if let Some(spec) = limits.hours_window() {
235 match parse_window(spec) {
236 Ok((start, end)) => {
237 let now_local = now.with_timezone(&Local).time();
238 if !in_window(now_local, start, end) {
239 return LimitOutcome::ThroughputReject(format!(
240 "outside hours window {spec} (now={})",
241 now_local.format("%H:%M")
242 ));
243 }
244 }
245 Err(e) => {
246 return LimitOutcome::ThroughputReject(format!(
247 "invalid hours_window {spec:?}: {e}"
248 ));
249 }
250 }
251 }
252
253 if let Some(value) = ctx.order_value {
257 if let Err(reason) = validate_order_value(value) {
258 return LimitOutcome::ValueReject(format!(
259 "order value invalid ({reason:?}): {value}"
260 ));
261 }
262 if let Some(cap) = limits.max_order_value()
263 && value > cap + f64::EPSILON
264 {
265 return LimitOutcome::ValueReject(format!(
266 "order value {value:.2} exceeds per-order cap {cap:.2}"
267 ));
268 }
269 }
270
271 if !ctx.mutation_no_exposure
278 && let (Some(value), Some(_)) = (ctx.order_value, limits.max_daily_value())
279 {
280 let daily_currency = ctx.daily_currency();
281 let today = now.date_naive();
282 let counter = self
283 .counters
284 .entry(key_id.to_string())
285 .or_insert_with(|| DailyCounter::new(today));
286 match counter.try_add(
287 daily_currency.as_deref(),
288 value,
289 limits.max_daily_value(),
290 today,
291 ) {
292 Ok(_) => {}
293 Err(DailyAddError::OverCap(msg)) => return LimitOutcome::ThroughputReject(msg),
294 Err(DailyAddError::Invalid(reason)) => {
295 return LimitOutcome::ValueReject(format!(
296 "order value invalid ({reason:?}): {value}"
297 ));
298 }
299 Err(DailyAddError::StaleDay {
300 guard_day,
301 current_day,
302 }) => return stale_day_outcome(guard_day, current_day),
303 Err(DailyAddError::Underflow {
304 currency,
305 current,
306 release,
307 }) => {
308 return LimitOutcome::ValueReject(format!(
309 "daily receipt release underflow for {currency}: current={current:.2}, release={release:.2}"
310 ));
311 }
312 }
313 }
314
315 LimitOutcome::Allow
316 }
317}