futu_auth/limits/runtime/
accepted_quota.rs1use super::*;
7use chrono::{DateTime, Local, Utc};
8
9impl<'a> LimitGuard<'a> {
10 pub fn commit_daily(mut self) -> Result<(), LimitOutcome> {
22 let Some((value, today, currency)) = self.pending_daily.take() else {
23 return Ok(());
24 };
25 let counter = self
26 .counters
27 .counters
28 .entry(self.key_id.clone())
29 .or_insert_with(|| DailyCounter::new(today));
30 match counter.try_add(currency.as_deref(), value, self.daily_cap, today) {
31 Ok(_) => Ok(()),
32 Err(DailyAddError::OverCap(msg)) => Err(LimitOutcome::ThroughputReject(msg)),
33 Err(DailyAddError::Invalid(reason)) => Err(LimitOutcome::ValueReject(format!(
34 "order value invalid ({reason:?}): {value}"
35 ))),
36 Err(DailyAddError::StaleDay {
37 guard_day,
38 current_day,
39 }) => Err(stale_day_outcome(guard_day, current_day)),
40 Err(DailyAddError::Underflow {
41 currency,
42 current,
43 release,
44 }) => Err(LimitOutcome::ValueReject(format!(
45 "daily receipt release underflow for {currency}: current={current:.2}, release={release:.2}"
46 ))),
47 }
48 }
49
50 #[must_use]
52 pub fn has_pending_daily(&self) -> bool {
53 self.pending_daily.is_some()
54 }
55}
56
57impl RuntimeCounters {
58 pub fn check_limits<'a>(
73 &'a self,
74 key_id: &str,
75 limits: &(impl LimitPolicy + ?Sized),
76 ctx: &CheckCtx,
77 now: DateTime<Utc>,
78 commit_rate: bool,
79 ) -> Result<LimitGuard<'a>, LimitOutcome> {
80 if let (Some(allowed), Some(id)) = (limits.allowed_acc_ids(), ctx.acc_id)
82 && !allowed.is_empty()
83 && !allowed.contains(&id)
84 {
85 return Err(LimitOutcome::WhitelistReject(format!(
86 "acc_id {id} not in allowed list {allowed:?}"
87 )));
88 }
89
90 if let Some(markets) = limits.allowed_markets()
92 && !markets.is_empty()
93 && !ctx.market.is_empty()
94 && !markets.contains(&ctx.market)
95 {
96 return Err(LimitOutcome::WhitelistReject(format!(
97 "market {:?} not in allowed list {:?}",
98 ctx.market, markets
99 )));
100 }
101
102 if let Some(symbols) = limits.allowed_symbols()
104 && !symbols.is_empty()
105 && !ctx.symbol.is_empty()
106 && !symbols.contains(&ctx.symbol)
107 {
108 return Err(LimitOutcome::WhitelistReject(format!(
109 "symbol {:?} not in allowed list",
110 ctx.symbol
111 )));
112 }
113
114 if let (Some(allowed), Some(side)) = (limits.allowed_trd_sides(), &ctx.trd_side)
116 && !allowed.is_empty()
117 && !allowed.contains(side)
118 {
119 return Err(LimitOutcome::WhitelistReject(format!(
120 "trd_side {side:?} not in allowed list {allowed:?}"
121 )));
122 }
123
124 if let Some(spec) = limits.hours_window() {
126 match parse_window(spec) {
127 Ok((start, end)) => {
128 let now_local = now.with_timezone(&Local).time();
129 if !in_window(now_local, start, end) {
130 return Err(LimitOutcome::ThroughputReject(format!(
131 "outside hours window {spec} (now={})",
132 now_local.format("%H:%M")
133 )));
134 }
135 }
136 Err(e) => {
137 return Err(LimitOutcome::ThroughputReject(format!(
138 "invalid hours_window {spec:?}: {e}"
139 )));
140 }
141 }
142 }
143
144 if let Some(value) = ctx.order_value {
146 if let Err(reason) = validate_order_value(value) {
147 return Err(LimitOutcome::ValueReject(format!(
148 "order value invalid ({reason:?}): {value}"
149 )));
150 }
151 if let Some(cap) = limits.max_order_value()
152 && value > cap + f64::EPSILON
153 {
154 return Err(LimitOutcome::ValueReject(format!(
155 "order value {value:.2} exceeds per-order cap {cap:.2}"
156 )));
157 }
158 }
159
160 if commit_rate && let Some(max) = limits.max_orders_per_minute() {
162 let window = self.rates.entry(key_id.to_string()).or_default();
163 if let Err(e) = window.try_record(now, max) {
164 return Err(LimitOutcome::ThroughputReject(e));
165 }
166 }
167
168 let pending_daily = if !ctx.mutation_no_exposure
172 && let (Some(value), Some(_)) = (ctx.order_value, limits.max_daily_value())
173 {
174 let daily_currency = ctx.daily_currency();
175 let today = now.date_naive();
176 let counter = self
177 .counters
178 .entry(key_id.to_string())
179 .or_insert_with(|| DailyCounter::new(today));
180 match counter.peek_add(
181 daily_currency.as_deref(),
182 value,
183 limits.max_daily_value(),
184 today,
185 ) {
186 Ok(_) => Some((value, today, daily_currency)),
187 Err(DailyAddError::OverCap(msg)) => {
188 return Err(LimitOutcome::ThroughputReject(msg));
189 }
190 Err(DailyAddError::Invalid(reason)) => {
191 return Err(LimitOutcome::ValueReject(format!(
192 "order value invalid ({reason:?}): {value}"
193 )));
194 }
195 Err(DailyAddError::StaleDay {
196 guard_day,
197 current_day,
198 }) => return Err(stale_day_outcome(guard_day, current_day)),
199 Err(DailyAddError::Underflow {
200 currency,
201 current,
202 release,
203 }) => {
204 return Err(LimitOutcome::ValueReject(format!(
205 "daily receipt release underflow for {currency}: current={current:.2}, release={release:.2}"
206 )));
207 }
208 }
209 } else {
210 None
211 };
212
213 Ok(LimitGuard {
214 counters: self,
215 key_id: key_id.to_string(),
216 pending_daily,
217 daily_cap: limits.max_daily_value(),
218 })
219 }
220}