Skip to main content

futu_cache/
qot_right.rs

1// 行情权限缓存
2// 存储从后端 CMD 6024 获取的行情权限和 API 额度信息
3// 对应 C++ INNData_Qot_Right + INNData_APIInterLimit
4//
5// v1.4.106 codex 1217 (5月1日) 7 finding 全 default ON 真实装:
6//   F1 [P1] epoch / freshness 状态机 + relogin/reconnect/replace 触发
7//   F2 [P1] US futures detail 0 值覆盖旧权限 (字段存在=覆盖)
8//   F3 [P2] CMD6024 validity predicate 扩为"任意 qot right 字段存在" (push_parser)
9//   F4 [P2] startup async refresh window 不暴露默认 quota / Unknown 权限 (sys.rs)
10//   F5 [P2] 6651/6006 push 闭环 + NotifyType_QotRight 广播 (push_parser + bridge)
11//   F6 [P2] request_qot_right 携带 quote_change_notify (双模式) (push_parser)
12//   F7 [P2] TestCmd / RemoteCmd 返结构化 RefreshReport (sys.rs)
13
14use std::sync::Arc;
15
16use parking_lot::{Mutex, RwLock};
17
18mod backend_apply;
19mod capability;
20#[cfg(test)]
21mod capability_tests;
22mod projection;
23mod reconnect;
24mod security_rules;
25mod state;
26
27pub use capability::{
28    QuoteCapability, QuoteCryptoPolicy, QuoteOrderBookPolicy, QuoteSnapshotPolicy,
29    resolve_orderbook_entitlement, resolve_quote_capability,
30};
31use futu_domain_qot_right::{
32    QOT_API_QUOTA_DEFAULT, QotRight6651Change, QotRightBackendCurrent, QotRightBackendProjection,
33    QotRightDirectAuthChange, QotRightPushCurrentFacts, QotRightRefreshCurrentFacts,
34    project_qot_right_6651_change_with_current, project_qot_right_backend_update,
35    project_qot_right_direct_auth_change, qot_right_after_push_changes, qot_right_mark_failed,
36    qot_right_mark_fresh, qot_right_mark_pending, qot_right_on_epoch_advanced,
37};
38pub use futu_domain_qot_right::{
39    QOT_MARKET_CC_SECURITY, QOT_RIGHT_BMP, QOT_RIGHT_LEVEL1, QOT_RIGHT_LEVEL2, QOT_RIGHT_LEVEL3,
40    QOT_RIGHT_NO, QOT_RIGHT_SF, QOT_RIGHT_UNKNOWN, QotRightBackendExtras, QotRightBackendUpdate,
41    QotRightFreshness, QotRightPushedQuoteChangeNotify as PushedQuoteChangeNotify,
42    SECURITY_TYPE_CRYPTO, SECURITY_TYPE_DRVT, SECURITY_TYPE_FUTURE, SECURITY_TYPE_INDEX,
43};
44pub use futu_domain_qot_subscription::{
45    JP_LV2_ORDER_STOCK, JP_LV2_ORDER_STOCK_FULL, LV2_ORDER_US_FUTURE, Lv2OrderGroupPlan,
46    SG_LV2_ORDER_STOCK, SG_LV2_ORDER_STOCK_ODD_LOT, US_LV2_ORDER_ARCA, US_LV2_ORDER_NASDAQ_TV,
47    US_LV2_ORDER_OVERNIGHT,
48};
49pub use projection::{UsFutureDetailAuths, UsIndexFlags, UsLv2Flags, UsOtcAuths};
50use reconnect::{QotRightPublicSnapshot, QotRightReconnectSnapshot};
51pub use reconnect::{
52    QotRightReconnectAccumulator, QotRightReconnectClaim, QotRightReconnectClaimKind,
53    QotRightReconnectHandoff, QotRightReconnectLifecycle, QotRightReconnectRegistration,
54    QotRightReconnectToken,
55};
56pub use security_rules::{
57    basic_qot_uses_us_pre_after_detail, has_lv2_order_groups_for_security, is_crypto_market,
58    lv2_order_groups_for_security, lv2_order_groups_for_security_with_dynamic,
59    lv2_order_uses_jp_main_cache, lv2_order_uses_sg_odd_lot_cache,
60    order_book_max_depth_for_security, order_book_read_uses_requested_count,
61    order_book_requires_accepted_lv2_push, order_book_requires_backend_full_depth,
62    order_book_uses_backend_side_count, snapshot_masks_hk_bmp_bid_ask,
63    us_lv2_order_uses_exchange_cache,
64};
65
66pub use state::{
67    QotRightBackendApplyOutcome, QotRightData, QotRightFullApplyFacts, QotRightRefreshReport,
68    QotRightStateMeta, has_set_qot_right_like_cpp,
69};
70use state::{next_reconnect_token, now_ms};
71
72#[derive(Clone)]
73pub struct QotRightCache {
74    data: Arc<RwLock<QotRightData>>,
75    meta: Arc<Mutex<QotRightStateMeta>>,
76}
77
78/// One causally consistent entitlement fact read under the cache's canonical
79/// `data -> meta` lock order.
80///
81/// Callers that derive a backend request from this value must additionally
82/// take the snapshot under the owning Platform route-publication terminal.
83/// That couples the entitlement generation to the route used for the request
84/// without holding cache locks across network I/O.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct FreshDigitalFutureEntitlement {
87    pub backend_generation: u64,
88    pub source_publication_epoch: Option<u64>,
89    pub has_digital_future_auth: bool,
90}
91
92impl Default for QotRightCache {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl QotRightCache {
99    pub fn new() -> Self {
100        Self {
101            data: Arc::new(RwLock::new(QotRightData::default())),
102            meta: Arc::new(Mutex::new(QotRightStateMeta::default())),
103        }
104    }
105
106    pub fn get(&self) -> QotRightData {
107        self.data.read().clone()
108    }
109
110    /// Test-only exact snapshot injection for handler precedence matrices.
111    /// Production rights remain owned by CMD6024/6032 and push apply paths.
112    #[cfg(any(test, feature = "test-util"))]
113    pub fn set_data_for_test(&self, data: QotRightData) {
114        *self.data.write() = data;
115        let mut meta = self.meta.lock();
116        meta.freshness = qot_right_mark_fresh();
117        meta.last_refresh_at_ms = now_ms();
118    }
119
120    /// Returns the dedicated digital-futures entitlement and its cache
121    /// generation atomically, or `None` unless the facts are fresh.
122    pub fn fresh_digital_future_entitlement(&self) -> Option<FreshDigitalFutureEntitlement> {
123        let data = self.data.read();
124        let meta = self.meta.lock();
125        let source_publication_epoch = meta.source_publication_epoch?;
126        meta.freshness
127            .is_fresh()
128            .then_some(FreshDigitalFutureEntitlement {
129                backend_generation: meta.backend_generation,
130                source_publication_epoch: Some(source_publication_epoch),
131                has_digital_future_auth: data.has_digital_future_auth,
132            })
133    }
134
135    pub fn refresh_current_facts(&self) -> QotRightRefreshCurrentFacts {
136        let d = self.data.read();
137        QotRightRefreshCurrentFacts {
138            hk_qot_right: d.hk_qot_right,
139            hk_future_qot_right: d.hk_future_qot_right,
140            hk_option_qot_right: d.hk_option_qot_right,
141            api_us_qot_right: d.api_us_qot_right,
142            has_us_utp: d.has_us_utp,
143            us_lv2_nasdaq_totalview_qot_right: d.us_lv2_nasdaq_totalview_qot_right,
144            sg_stock_qot_right: d.sg_stock_qot_right,
145            jp_stock_qot_right: d.jp_stock_qot_right,
146            us_cme_future_qot_right: d.us_cme_future_qot_right,
147            us_cbot_future_qot_right: d.us_cbot_future_qot_right,
148            us_nymex_future_qot_right: d.us_nymex_future_qot_right,
149            us_comex_future_qot_right: d.us_comex_future_qot_right,
150            sg_future_qot_right: d.sg_future_qot_right,
151            us_otc_qot_right: d.us_otc_qot_right,
152            us_index_qot_right: d.us_index_qot_right,
153            jp_future_qot_right: d.jp_future_qot_right,
154            sh_qot_right: d.sh_qot_right,
155            sz_qot_right: d.sz_qot_right,
156        }
157    }
158
159    pub fn freshness(&self) -> QotRightFreshness {
160        self.meta.lock().freshness.clone()
161    }
162
163    pub fn is_fresh(&self) -> bool {
164        self.meta.lock().freshness.is_fresh()
165    }
166
167    pub fn meta_snapshot(&self) -> QotRightStateMeta {
168        self.meta.lock().clone()
169    }
170
171    pub fn take_event_contract_category_preload_request(&self) -> bool {
172        let mut meta = self.meta.lock();
173        std::mem::take(&mut meta.event_contract_category_preload_pending)
174    }
175
176    pub fn set_orderbook_depths(&self, hk_option_depth: Option<u32>, hk_future_depth: Option<u32>) {
177        let mut d = self.data.write();
178        if let Some(v) = hk_option_depth {
179            d.hk_option_orderbook_depth = Some(v);
180        }
181        if let Some(v) = hk_future_depth {
182            d.hk_future_orderbook_depth = Some(v);
183        }
184    }
185
186    pub fn mark_stale(&self, reason: &str) {
187        let mut m = self.meta.lock();
188        let old = m.freshness.clone();
189        m.source_publication_epoch = None;
190        m.freshness = QotRightFreshness::Stale;
191        tracing::info!(
192            old_freshness = ?old,
193            reason,
194            login_epoch = m.login_epoch,
195            backend_generation = m.backend_generation,
196            "QotRightCache: marked stale"
197        );
198    }
199
200    pub fn advance_login_epoch(&self, new_epoch: u64, user_id: Option<u64>) {
201        let mut m = self.meta.lock();
202        let old_epoch = m.login_epoch;
203        m.login_epoch = new_epoch;
204        m.user_id = user_id;
205        m.source_publication_epoch = None;
206        m.freshness = qot_right_on_epoch_advanced(m.freshness.clone());
207        let user_id_fp = user_id.map(futu_core::log_redact::uid_log_fingerprint);
208        tracing::info!(
209            old_epoch,
210            new_epoch,
211            user_id_fp = ?user_id_fp,
212            "QotRightCache: login epoch advanced"
213        );
214    }
215
216    pub fn advance_backend_generation(&self, new_generation: u64) {
217        let mut m = self.meta.lock();
218        let old_gen = m.backend_generation;
219        m.backend_generation = new_generation;
220        m.source_publication_epoch = None;
221        m.freshness = qot_right_on_epoch_advanced(m.freshness.clone());
222        tracing::info!(
223            old_generation = old_gen,
224            new_generation,
225            "QotRightCache: backend generation advanced"
226        );
227    }
228
229    pub fn mark_pending(&self) {
230        let mut meta = self.meta.lock();
231        meta.source_publication_epoch = None;
232        meta.freshness = qot_right_mark_pending();
233    }
234
235    pub fn mark_pending_if_generation(&self, expected_generation: u64) -> bool {
236        let mut meta = self.meta.lock();
237        if meta.backend_generation != expected_generation {
238            return false;
239        }
240        meta.source_publication_epoch = None;
241        meta.freshness = qot_right_mark_pending();
242        true
243    }
244
245    pub fn mark_fresh(&self) {
246        let mut m = self.meta.lock();
247        m.freshness = qot_right_mark_fresh();
248        m.last_refresh_at_ms = now_ms();
249    }
250
251    pub fn mark_failed(&self, error: impl Into<String>) {
252        let mut meta = self.meta.lock();
253        meta.source_publication_epoch = None;
254        meta.freshness = qot_right_mark_failed(error, now_ms());
255    }
256
257    pub fn mark_failed_if_generation(
258        &self,
259        expected_generation: u64,
260        error: impl Into<String>,
261    ) -> bool {
262        let mut meta = self.meta.lock();
263        if meta.backend_generation != expected_generation {
264            return false;
265        }
266        meta.source_publication_epoch = None;
267        meta.freshness = qot_right_mark_failed(error, now_ms());
268        true
269    }
270
271    pub fn last_refresh_at_ms(&self) -> i64 {
272        self.meta.lock().last_refresh_at_ms
273    }
274
275    pub fn set_pushed_quote_change_notify(&self, notify: PushedQuoteChangeNotify) {
276        let mut meta = self.meta.lock();
277        meta.last_pushed_quote_change_notify_version = meta
278            .last_pushed_quote_change_notify_version
279            .saturating_add(1);
280        meta.last_pushed_quote_change_notify = Some(notify);
281    }
282
283    pub fn pushed_quote_change_notify(&self) -> Option<PushedQuoteChangeNotify> {
284        self.meta.lock().last_pushed_quote_change_notify.clone()
285    }
286
287    pub fn pushed_quote_change_notify_snapshot(&self) -> (u64, Option<PushedQuoteChangeNotify>) {
288        let meta = self.meta.lock();
289        (
290            meta.last_pushed_quote_change_notify_version,
291            meta.last_pushed_quote_change_notify.clone(),
292        )
293    }
294}
295
296#[cfg(test)]
297mod tests;