futu_server/subscription/views.rs
1use std::collections::{HashMap, HashSet};
2use std::hash::Hash;
3use std::time::Duration;
4
5use futu_core::qot_stock_key::QotSecurityKey;
6
7use super::SubscriptionManager;
8
9impl SubscriptionManager {
10 // ===== Quota =====
11
12 /// per-conn used quota — count 该 conn 的 (security_key, sub_type) 对数.
13 pub fn get_conn_used_quota(&self, conn_id: u64) -> u32 {
14 self.qot_subs
15 .read()
16 .iter()
17 .filter(|(_, set)| set.contains(&conn_id))
18 .count() as u32
19 }
20
21 /// per-conn used quota with a caller-provided key classifier.
22 ///
23 /// C++ 10.7 separates normal subscription quota from option-chain quota:
24 /// `QotSubscribe::GetUsedQuota` skips keys resolvable by
25 /// `ResolveOptionChainKey`; `GetUsedOptionQuota` handles those separately.
26 /// Rust keeps the option classifier outside `SubscriptionManager` because
27 /// it depends on `StaticDataCache` metadata.
28 pub fn get_conn_used_quota_by<F>(&self, conn_id: u64, include: F) -> u32
29 where
30 F: Fn(&QotSecurityKey) -> bool,
31 {
32 self.qot_subs
33 .read()
34 .iter()
35 .filter(|((key, _), set)| set.contains(&conn_id) && include(key))
36 .count() as u32
37 }
38
39 /// **全局 used quota** — 对齐 C++ `m_nAllUsedQuota` (累加全局唯一
40 /// SubKey count). 不重复计 conn — 多 conn 订同 (stock, sub_type) 全局算 1.
41 pub fn get_total_used_quota(&self) -> u32 {
42 self.qot_subs.read().len() as u32
43 }
44
45 /// Global used quota with a caller-provided key classifier.
46 pub fn get_total_used_quota_by<F>(&self, include: F) -> u32
47 where
48 F: Fn(&QotSecurityKey) -> bool,
49 {
50 self.qot_subs
51 .read()
52 .iter()
53 .filter(|((key, _), set)| !set.is_empty() && include(key))
54 .count() as u32
55 }
56
57 /// Global option-chain quota, deduped by `(option_chain_key, sub_type)`.
58 ///
59 /// The supplied closure mirrors C++ `ResolveOptionChainKey`: `None` means
60 /// the key is a normal security; `Some(chain)` means it participates in the
61 /// option quota bill set.
62 pub fn get_total_used_option_quota_by<F, K>(&self, option_bill_key: F) -> u32
63 where
64 F: Fn(&QotSecurityKey) -> Option<K>,
65 K: Eq + Hash,
66 {
67 let mut bills = HashSet::new();
68 for ((key, sub_type), set) in self.qot_subs.read().iter() {
69 if set.is_empty() {
70 continue;
71 }
72 if let Some(chain) = option_bill_key(key) {
73 bills.insert((chain, *sub_type));
74 }
75 }
76 bills.len() as u32
77 }
78
79 /// Per-connection option-chain quota, deduped by `(option_chain_key, sub_type)`.
80 pub fn get_conn_used_option_quota_by<F, K>(&self, conn_id: u64, option_bill_key: F) -> u32
81 where
82 F: Fn(&QotSecurityKey) -> Option<K>,
83 K: Eq + Hash,
84 {
85 let mut bills = HashSet::new();
86 for ((key, sub_type), set) in self.qot_subs.read().iter() {
87 if !set.contains(&conn_id) {
88 continue;
89 }
90 if let Some(chain) = option_bill_key(key) {
91 bills.insert((chain, *sub_type));
92 }
93 }
94 bills.len() as u32
95 }
96
97 // ===== Conn-level views =====
98
99 pub fn get_conn_qot_subs(&self, conn_id: u64) -> HashMap<i32, Vec<String>> {
100 let qot = self.qot_subs.read();
101 let mut result: HashMap<i32, Vec<String>> = HashMap::new();
102 for ((key, sub_type), conn_ids) in qot.iter() {
103 if conn_ids.contains(&conn_id) {
104 result.entry(*sub_type).or_default().push(key.cache_key());
105 }
106 }
107 result
108 }
109
110 pub fn get_all_qot_conn_ids(&self) -> HashSet<u64> {
111 let qot = self.qot_subs.read();
112 let mut ids = HashSet::new();
113 for conn_ids in qot.values() {
114 ids.extend(conn_ids);
115 }
116 ids
117 }
118
119 /// v1.4.106 codex 0932 F5 [P2]: 获取所有连接 ID(有交易账户订阅的).
120 pub fn get_all_trd_conn_ids(&self) -> HashSet<u64> {
121 let trd = self.trd_acc_subs.read();
122 let mut ids = HashSet::new();
123 for conn_ids in trd.values() {
124 ids.extend(conn_ids);
125 }
126 ids
127 }
128
129 /// v1.4.106 codex 1131 F2 [P1]: 计算全局 desired set.
130 /// 返 (sec_key_display, sub_type), sec_key_display 是 cache_key 形态
131 /// (`"market_code"` or `"market_code@b{id}"`).
132 pub fn compute_global_desired_set(&self) -> Vec<(String, i32)> {
133 let qot = self.qot_subs.read();
134 let mut out = Vec::with_capacity(qot.len());
135 for (k, sub_type) in qot.keys() {
136 out.push((k.cache_key(), *sub_type));
137 }
138 out
139 }
140
141 /// **v1.4.106 codex 0631 F2 [P2]**: shared global desired-set keys helper.
142 ///
143 /// 返当前全集 `Vec<(sec_key, sub_type)>` (与 `compute_global_desired_set`
144 /// 等价, 命名对齐 codex 0631 audit 习惯). 共享给 SubHandler / unsub /
145 /// unsub_all / resubscribe_quotes 三路径, 不再每条 sub 单独发 delta —
146 /// 对齐 CMD 6211 set-state 协议 (per-market full set replaces).
147 ///
148 /// caller 应做的事:
149 /// 1. 调本 fn 拿当前全集
150 /// 2. 应用 delta (sub: 加; unsub: 减; unsub_all: 移除本 conn 独占的)
151 /// 3. resolve sec_key → stock_id (cache); cache miss → loud warn (本 fn 不
152 /// 替 caller 决定 — 由 caller 选择 fail-loud 还是 partial-submit)
153 /// 4. 调 `submit_global_desired_set(NEW set)` 让 backend ack
154 ///
155 /// 命名: keys = `(sec_key, sub_type)` 二元组, 不是 `(stock_id, market)` —
156 /// stock_id resolve 是 caller 责任 (因为依赖 StaticDataCache).
157 #[inline]
158 pub fn qot_global_desired_keys(&self) -> Vec<(String, i32)> {
159 self.compute_global_desired_set()
160 }
161
162 /// C++ delay-statistics gate equivalent for QOT push reporting.
163 ///
164 /// Ref: `APIServer_Qot_StockBasic.cpp:218-223`,
165 /// `APIServer_Qot_OrderBook.cpp:238-243`, and
166 /// `APIServer_Qot_Broker.cpp:194-199`: `IsHasSubFewTime(..., 3)` must be
167 /// true before `Push_Count_Add`.
168 pub fn qot_sub_elapsed_at_least_broker(
169 &self,
170 sec_key: &QotSecurityKey,
171 sub_type: i32,
172 elapsed: Duration,
173 ) -> bool {
174 self.qot_sub_times
175 .read()
176 .get(&(Self::broker_key(sec_key), sub_type))
177 .map(|instant| instant.elapsed() >= elapsed)
178 .unwrap_or(true)
179 }
180}