1use parking_lot::{Mutex, RwLock};
15
16mod capability;
17#[cfg(test)]
18mod capability_tests;
19mod projection;
20mod security_rules;
21
22pub use capability::{
23 QuoteCapability, QuoteCryptoPolicy, QuoteOrderBookPolicy, QuoteSnapshotPolicy,
24 resolve_quote_capability,
25};
26pub use projection::{UsFutureDetailAuths, UsIndexFlags, UsLv2Flags, UsOtcAuths};
27use projection::{
28 cn_clt_to_api, crypto_auth_to_api, has_any_us_lv2_flag, hk_clt_to_api, jp_stock_clt_to_api,
29 my_stock_common_clt_to_api, other_clt_to_api, us_backend_status_to_api, us_clt_to_api,
30 us_future_clt_to_api, us_index_flags_to_api, us_option_clt_to_api, us_otc_comm_auth_to_api,
31};
32pub use security_rules::{
33 basic_qot_uses_us_pre_after_detail, has_merged_lv2_order_subs_for_security, is_crypto_market,
34 lv2_order_uses_jp_main_cache, lv2_order_uses_sg_odd_lot_cache,
35 merged_lv2_order_subs_for_security, order_book_max_depth_for_security,
36 order_book_read_uses_requested_count, order_book_requires_accepted_lv2_push,
37 order_book_requires_backend_full_depth, order_book_uses_backend_side_count,
38 snapshot_masks_hk_bmp_bid_ask, us_lv2_order_uses_exchange_cache,
39};
40
41const QOT_RIGHT_UNKNOWN: i32 = 0;
42const QOT_RIGHT_BMP: i32 = 1;
43const QOT_RIGHT_LEVEL1: i32 = 2;
44const QOT_RIGHT_LEVEL2: i32 = 3;
45pub const QOT_RIGHT_SF: i32 = 4;
46const QOT_RIGHT_NO: i32 = 5;
47const QOT_RIGHT_LEVEL3: i32 = 6;
48const SECURITY_TYPE_INDEX: i32 = 6;
49const SECURITY_TYPE_DRVT: i32 = 8;
50const SECURITY_TYPE_FUTURE: i32 = 10;
51const SECURITY_TYPE_CRYPTO: i32 = 12;
52const QOT_MARKET_CC_SECURITY: i32 = 91;
53pub const US_LV2_ORDER_ARCA: u32 = 1;
54pub const US_LV2_ORDER_NASDAQ_TV: u32 = 4;
55pub const US_LV2_ORDER_OVERNIGHT: u32 = 128;
56pub const LV2_ORDER_US_FUTURE: u32 = 1;
57pub const SG_LV2_ORDER_STOCK: u32 = 1;
58pub const SG_LV2_ORDER_STOCK_ODD_LOT: u32 = 2;
59pub const JP_LV2_ORDER_STOCK: u32 = 1;
62pub const JP_LV2_ORDER_STOCK_FULL: u32 = 2;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct Lv2OrderSubDescriptor {
66 pub lv2_type: u32,
67 pub level: u32,
68 pub prob2_v2: bool,
69}
70
71#[derive(Debug, Clone, Copy, Default)]
72pub struct QotRightBackendExtras {
73 pub us_lv2_flags: Option<UsLv2Flags>,
74 pub us_index_flags: Option<UsIndexFlags>,
75 pub us_otc_auths: Option<UsOtcAuths>,
76}
77
78impl QotRightBackendExtras {
79 pub const fn none() -> Self {
80 Self {
81 us_lv2_flags: None,
82 us_index_flags: None,
83 us_otc_auths: None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, Default)]
89pub struct QotRightBackendUpdate {
90 pub hk_got: Option<u32>,
91 pub us_got: Option<u32>,
92 pub cn_got: Option<u32>,
93 pub sh_auth: Option<u32>,
94 pub sz_auth: Option<u32>,
95 pub hk_option: Option<u32>,
96 pub hk_future: Option<u32>,
97 pub us_option: Option<u32>,
98 pub us_future_cme_cboe: Option<u32>,
99 pub us_future_detail: Option<UsFutureDetailAuths>,
100 pub sg_future: Option<u32>,
101 pub jp_future: Option<u32>,
102 pub sg_stock: Option<u32>,
103 pub my_stock: Option<u32>,
104 pub jp_stock: Option<u32>,
105 pub digital_currency_auth: Option<u32>,
106 pub digital_pt_orderbook_auth: Option<u32>,
107 pub sub_limit: Option<u32>,
108 pub kl_limit: Option<u32>,
109 pub extras: QotRightBackendExtras,
110}
111
112#[derive(Debug, Clone)]
113pub struct QotRightData {
114 pub hk_qot_right: i32,
115 pub us_qot_right: i32,
116 pub api_us_qot_right: i32,
118 pub sh_qot_right: i32,
119 pub sz_qot_right: i32,
120 pub hk_option_qot_right: i32,
121 pub hk_future_qot_right: i32,
122 pub has_us_option_qot_right: bool,
123 pub us_option_qot_right: i32,
124 pub us_index_qot_right: i32,
125 pub us_otc_qot_right: i32,
126 pub us_cme_future_qot_right: i32,
127 pub us_cbot_future_qot_right: i32,
128 pub us_nymex_future_qot_right: i32,
129 pub us_comex_future_qot_right: i32,
130 pub us_cboe_future_qot_right: i32,
131 pub sg_future_qot_right: i32,
132 pub jp_future_qot_right: i32,
133 pub sg_stock_qot_right: i32,
134 pub my_stock_qot_right: i32,
135 pub jp_stock_qot_right: i32,
136 pub cc_qot_right: i32,
137 pub cc_pt_orderbook_qot_right: i32,
138 pub us_lv2_arca_qot_right: bool,
139 pub us_lv2_nyse_qot_right: bool,
140 pub us_lv2_nasdaq_totalview_qot_right: bool,
141 pub sub_quota: i32,
142 pub history_kl_quota: i32,
143 pub hk_option_orderbook_depth: Option<u32>,
145 pub hk_future_orderbook_depth: Option<u32>,
146}
147
148impl Default for QotRightData {
149 fn default() -> Self {
150 const UNKNOWN: i32 = 0;
151 Self {
152 hk_qot_right: UNKNOWN,
153 us_qot_right: UNKNOWN,
154 api_us_qot_right: UNKNOWN,
155 sh_qot_right: UNKNOWN,
156 sz_qot_right: UNKNOWN,
157 hk_option_qot_right: UNKNOWN,
158 hk_future_qot_right: UNKNOWN,
159 has_us_option_qot_right: false,
162 us_option_qot_right: UNKNOWN,
163 us_index_qot_right: UNKNOWN,
164 us_otc_qot_right: UNKNOWN,
165 us_cme_future_qot_right: UNKNOWN,
166 us_cbot_future_qot_right: UNKNOWN,
167 us_nymex_future_qot_right: UNKNOWN,
168 us_comex_future_qot_right: UNKNOWN,
169 us_cboe_future_qot_right: UNKNOWN,
170 sg_future_qot_right: UNKNOWN,
171 jp_future_qot_right: UNKNOWN,
172 sg_stock_qot_right: UNKNOWN,
173 my_stock_qot_right: UNKNOWN,
174 jp_stock_qot_right: UNKNOWN,
175 cc_qot_right: UNKNOWN,
176 cc_pt_orderbook_qot_right: UNKNOWN,
177 us_lv2_arca_qot_right: false,
178 us_lv2_nyse_qot_right: false,
179 us_lv2_nasdaq_totalview_qot_right: false,
180 sub_quota: 4000,
181 history_kl_quota: 100,
182 hk_option_orderbook_depth: None,
183 hk_future_orderbook_depth: None,
184 }
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum QotRightFreshness {
192 Unknown,
193 Pending,
194 Fresh,
195 Stale,
196 Failed { error: String, since_ms: i64 },
197}
198
199impl QotRightFreshness {
200 pub fn is_fresh(&self) -> bool {
201 matches!(self, QotRightFreshness::Fresh)
202 }
203 pub fn is_unconfirmed(&self) -> bool {
204 !self.is_fresh()
205 }
206}
207
208#[derive(Debug, Clone, Default)]
209pub struct PushedQuoteChangeNotify {
210 pub change_items: Vec<(i32, i32, i32)>,
211}
212
213#[derive(Debug, Clone)]
214pub struct QotRightRefreshReport {
215 pub changed: bool,
216 pub fresh_at_ms: i64,
217 pub decoded_fields: Vec<&'static str>,
218 pub freshness_after: QotRightFreshness,
219 pub login_epoch: u64,
220}
221
222#[derive(Debug, Clone)]
223pub struct QotRightStateMeta {
224 pub freshness: QotRightFreshness,
225 pub user_id: Option<u64>,
226 pub login_epoch: u64,
227 pub backend_generation: u64,
228 pub last_refresh_at_ms: i64,
229 pub last_pushed_quote_change_notify: Option<PushedQuoteChangeNotify>,
230}
231
232impl Default for QotRightStateMeta {
233 fn default() -> Self {
234 Self {
235 freshness: QotRightFreshness::Unknown,
236 user_id: None,
237 login_epoch: 0,
238 backend_generation: 0,
239 last_refresh_at_ms: 0,
240 last_pushed_quote_change_notify: None,
241 }
242 }
243}
244
245fn now_ms() -> i64 {
246 use std::time::{SystemTime, UNIX_EPOCH};
247 SystemTime::now()
248 .duration_since(UNIX_EPOCH)
249 .map(|d| d.as_millis() as i64)
250 .unwrap_or(0)
251}
252
253pub struct QotRightCache {
254 data: RwLock<QotRightData>,
255 meta: Mutex<QotRightStateMeta>,
256}
257
258impl Default for QotRightCache {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264impl QotRightCache {
265 pub fn new() -> Self {
266 Self {
267 data: RwLock::new(QotRightData::default()),
268 meta: Mutex::new(QotRightStateMeta::default()),
269 }
270 }
271
272 pub fn get(&self) -> QotRightData {
273 self.data.read().clone()
274 }
275
276 pub fn freshness(&self) -> QotRightFreshness {
277 self.meta.lock().freshness.clone()
278 }
279
280 pub fn is_fresh(&self) -> bool {
281 self.meta.lock().freshness.is_fresh()
282 }
283
284 pub fn meta_snapshot(&self) -> QotRightStateMeta {
285 self.meta.lock().clone()
286 }
287
288 pub fn set_orderbook_depths(&self, hk_option_depth: Option<u32>, hk_future_depth: Option<u32>) {
289 let mut d = self.data.write();
290 if let Some(v) = hk_option_depth {
291 d.hk_option_orderbook_depth = Some(v);
292 }
293 if let Some(v) = hk_future_depth {
294 d.hk_future_orderbook_depth = Some(v);
295 }
296 }
297
298 pub fn mark_stale(&self, reason: &str) {
299 let mut m = self.meta.lock();
300 let old = m.freshness.clone();
301 m.freshness = QotRightFreshness::Stale;
302 tracing::info!(
303 old_freshness = ?old,
304 reason,
305 login_epoch = m.login_epoch,
306 backend_generation = m.backend_generation,
307 "QotRightCache: marked stale"
308 );
309 }
310
311 pub fn advance_login_epoch(&self, new_epoch: u64, user_id: Option<u64>) {
312 let mut m = self.meta.lock();
313 let old_epoch = m.login_epoch;
314 m.login_epoch = new_epoch;
315 m.user_id = user_id;
316 if matches!(m.freshness, QotRightFreshness::Fresh) {
317 m.freshness = QotRightFreshness::Stale;
318 }
319 tracing::info!(
320 old_epoch,
321 new_epoch,
322 user_id = ?user_id,
323 "QotRightCache: login epoch advanced"
324 );
325 }
326
327 pub fn advance_backend_generation(&self, new_generation: u64) {
328 let mut m = self.meta.lock();
329 let old_gen = m.backend_generation;
330 m.backend_generation = new_generation;
331 if matches!(m.freshness, QotRightFreshness::Fresh) {
332 m.freshness = QotRightFreshness::Stale;
333 }
334 tracing::info!(
335 old_generation = old_gen,
336 new_generation,
337 "QotRightCache: backend generation advanced"
338 );
339 }
340
341 pub fn mark_pending(&self) {
342 self.meta.lock().freshness = QotRightFreshness::Pending;
343 }
344
345 pub fn mark_fresh(&self) {
346 let mut m = self.meta.lock();
347 m.freshness = QotRightFreshness::Fresh;
348 m.last_refresh_at_ms = now_ms();
349 }
350
351 pub fn mark_failed(&self, error: impl Into<String>) {
352 self.meta.lock().freshness = QotRightFreshness::Failed {
353 error: error.into(),
354 since_ms: now_ms(),
355 };
356 }
357
358 pub fn last_refresh_at_ms(&self) -> i64 {
359 self.meta.lock().last_refresh_at_ms
360 }
361
362 pub fn set_pushed_quote_change_notify(&self, notify: PushedQuoteChangeNotify) {
363 self.meta.lock().last_pushed_quote_change_notify = Some(notify);
364 }
365
366 pub fn pushed_quote_change_notify(&self) -> Option<PushedQuoteChangeNotify> {
367 self.meta.lock().last_pushed_quote_change_notify.clone()
368 }
369
370 pub fn apply_6651_changes(&self, items: &[(i32, i32, i32)]) -> Vec<i32> {
371 let mut d = self.data.write();
372 let mut changed_types = Vec::new();
373
374 for &(quote_type, before, after) in items {
375 if before == 0 || before == after {
378 continue;
379 }
380 let after_u = after as u32;
381 if apply_qot_right_after_change(&mut d, quote_type, after_u) {
382 changed_types.push(quote_type);
383 }
384 }
385 if !changed_types.is_empty() {
386 drop(d);
387 let mut m = self.meta.lock();
388 m.freshness = QotRightFreshness::Fresh;
389 m.last_refresh_at_ms = now_ms();
390 }
391 changed_types
392 }
393
394 pub fn apply_direct_auth_changes(&self, items: &[(i32, u32)]) -> Vec<i32> {
395 let mut d = self.data.write();
396 let mut changed_types = Vec::new();
397
398 for &(quote_type, after) in items {
399 if apply_qot_right_after_change(&mut d, quote_type, after) {
400 changed_types.push(quote_type);
401 }
402 }
403 if !changed_types.is_empty() {
404 drop(d);
405 let mut m = self.meta.lock();
406 m.freshness = QotRightFreshness::Fresh;
407 m.last_refresh_at_ms = now_ms();
408 }
409 changed_types
410 }
411
412 pub fn update_from_backend(&self, update: QotRightBackendUpdate) {
421 let QotRightBackendUpdate {
422 hk_got,
423 us_got,
424 cn_got,
425 sh_auth,
426 sz_auth,
427 hk_option,
428 hk_future,
429 us_option,
430 us_future_cme_cboe,
431 us_future_detail,
432 sg_future,
433 jp_future,
434 sg_stock,
435 my_stock,
436 jp_stock,
437 digital_currency_auth,
438 digital_pt_orderbook_auth,
439 sub_limit,
440 kl_limit,
441 extras,
442 } = update;
443 let mut d = self.data.write();
444
445 if let Some(v) = hk_got {
446 d.hk_qot_right = hk_clt_to_api(v);
447 }
448 if let Some(v) = us_got {
449 let right = us_clt_to_api(v);
450 d.us_qot_right = right;
451 }
452 if let Some(right) = us_backend_status_to_api(us_got, extras.us_lv2_flags) {
453 d.api_us_qot_right = right;
454 }
455 if let Some(flags) = extras.us_lv2_flags {
456 let (arca, nyse, nasdaq_tv, _edg, _bzx) = flags;
457 d.us_lv2_arca_qot_right = arca == Some(1);
458 d.us_lv2_nyse_qot_right = nyse == Some(1);
459 d.us_lv2_nasdaq_totalview_qot_right = nasdaq_tv == Some(1);
460 if has_any_us_lv2_flag(flags) && matches!(d.us_qot_right, 2 | 3) {
461 d.us_qot_right = 3;
465 }
466 }
467 if let Some((dow_jones, nasdaq, standard_poor)) = extras.us_index_flags
468 && let Some(api_right) = us_index_flags_to_api(dow_jones, nasdaq, standard_poor)
469 {
470 d.us_index_qot_right = api_right;
471 }
472 if let Some((deal_data_auth, order_book_auth)) = extras.us_otc_auths {
473 d.us_otc_qot_right = us_otc_comm_auth_to_api(deal_data_auth, order_book_auth);
474 }
475 if let Some(v) = sh_auth {
476 d.sh_qot_right = cn_clt_to_api(v);
477 } else if let Some(v) = cn_got {
478 d.sh_qot_right = cn_clt_to_api(v);
479 }
480 if let Some(v) = sz_auth {
481 d.sz_qot_right = cn_clt_to_api(v);
482 } else if let Some(v) = cn_got {
483 d.sz_qot_right = cn_clt_to_api(v);
484 }
485 if let Some(v) = hk_option {
486 d.hk_option_qot_right = hk_clt_to_api(v);
487 }
488 if let Some(v) = hk_future {
489 d.hk_future_qot_right = hk_clt_to_api(v);
490 }
491 if let Some(v) = us_option {
492 d.us_option_qot_right = us_option_clt_to_api(v);
493 d.has_us_option_qot_right = d.us_option_qot_right != 1;
494 }
495 if let Some(v) = us_future_cme_cboe {
496 d.us_cme_future_qot_right = us_future_clt_to_api(v);
497 d.us_cboe_future_qot_right = 5;
502 }
503
504 if let Some((cme, cbot, nymex, comex)) = us_future_detail {
507 if let Some(cme) = cme {
508 d.us_cme_future_qot_right = us_future_clt_to_api(cme);
509 }
510 if let Some(cbot) = cbot {
511 d.us_cbot_future_qot_right = us_future_clt_to_api(cbot);
512 }
513 if let Some(nymex) = nymex {
514 d.us_nymex_future_qot_right = us_future_clt_to_api(nymex);
515 }
516 if let Some(comex) = comex {
517 d.us_comex_future_qot_right = us_future_clt_to_api(comex);
518 }
519 d.us_cboe_future_qot_right = 5;
521 }
522
523 if let Some(v) = sg_future {
524 d.sg_future_qot_right = other_clt_to_api(v);
525 }
526 if let Some(v) = jp_future {
527 d.jp_future_qot_right = other_clt_to_api(v);
528 }
529 if let Some(v) = sg_stock {
530 d.sg_stock_qot_right = other_clt_to_api(v);
531 }
532 if let Some(v) = my_stock {
533 d.my_stock_qot_right = my_stock_common_clt_to_api(v);
534 }
535 if let Some(v) = jp_stock {
536 d.jp_stock_qot_right = jp_stock_clt_to_api(v);
537 }
538 if let Some(v) = digital_currency_auth {
539 d.cc_qot_right = crypto_auth_to_api(v);
540 }
541 if let Some(v) = digital_pt_orderbook_auth {
542 d.cc_pt_orderbook_qot_right = crypto_auth_to_api(v);
543 }
544
545 if let Some(v) = sub_limit
546 && v > 0
547 {
548 d.sub_quota = v as i32;
549 }
550 if let Some(v) = kl_limit
551 && v > 0
552 {
553 d.history_kl_quota = v as i32;
554 }
555
556 drop(d);
558 let mut m = self.meta.lock();
559 m.freshness = QotRightFreshness::Fresh;
560 m.last_refresh_at_ms = now_ms();
561 }
562}
563
564fn apply_qot_right_after_change(data: &mut QotRightData, quote_type: i32, after: u32) -> bool {
565 match quote_type {
566 1 => data.hk_qot_right = hk_clt_to_api(after),
567 3 | 17 => {
568 let right = us_clt_to_api(after);
569 data.us_qot_right = right;
570 data.api_us_qot_right = right;
571 }
572 4 => data.hk_future_qot_right = hk_clt_to_api(after),
573 5 => data.hk_option_qot_right = hk_clt_to_api(after),
574 36 => data.sg_future_qot_right = other_clt_to_api(after),
575 39 => data.jp_future_qot_right = other_clt_to_api(after),
576 20 => data.sg_stock_qot_right = other_clt_to_api(after),
577 29 => data.jp_stock_qot_right = jp_stock_clt_to_api(after),
578 30 => data.us_cme_future_qot_right = us_future_clt_to_api(after),
579 31 => data.us_cbot_future_qot_right = us_future_clt_to_api(after),
580 32 => data.us_nymex_future_qot_right = us_future_clt_to_api(after),
581 33 => data.us_comex_future_qot_right = us_future_clt_to_api(after),
582 37 => data.us_otc_qot_right = if after == 2 { 2 } else { 5 },
583 38 => data.us_index_qot_right = us_clt_to_api(after),
584 40 => data.sh_qot_right = cn_clt_to_api(after),
585 41 => data.sz_qot_right = cn_clt_to_api(after),
586 _ => return false,
587 }
588 true
589}
590
591#[cfg(test)]
592mod tests;