Skip to main content

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}