Skip to main content

futu_cache/qot_right/
state.rs

1//! Split from qot_right.rs: entitlement state model.
2//!
3//! `QotRightData` (CMD 6024 行情权限 + API 额度), 刷新/apply 报告结构,
4//! `QotRightStateMeta` (epoch / freshness / reconnect lifecycle) 与私有 helper.
5
6use super::*;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct QotRightData {
10    /// C++ `m_enQotRightHK`; internal `HasSetQotRight` fact.
11    pub hk_own_qot_right: i32,
12    pub hk_qot_right: i32,
13    /// C++ `m_enQotRightUS`; internal `HasSetQotRight` fact.
14    pub us_own_qot_right: i32,
15    pub us_qot_right: i32,
16    /// C++ `INNData_Qot_Right::GetUSQotStatus()` projection for GetUserInfo/Notify.
17    pub api_us_qot_right: i32,
18    /// C++ `INNData_Qot_Right::HasUSUtp()`; drives real CMD3020 quote_level.
19    pub has_us_utp: bool,
20    /// CMD6024 field 67: exact US overnight entitlement (LV1=1).
21    pub has_us_overnight: bool,
22    pub sh_qot_right: i32,
23    pub sz_qot_right: i32,
24    pub hk_option_qot_right: i32,
25    pub hk_future_qot_right: i32,
26    pub has_us_option_qot_right: bool,
27    pub us_option_qot_right: i32,
28    pub us_index_qot_right: i32,
29    pub us_otc_qot_right: i32,
30    pub us_cme_future_qot_right: i32,
31    pub us_cbot_future_qot_right: i32,
32    pub us_nymex_future_qot_right: i32,
33    pub us_comex_future_qot_right: i32,
34    pub us_cboe_future_qot_right: i32,
35    pub sg_future_qot_right: i32,
36    pub jp_future_qot_right: i32,
37    pub sg_stock_qot_right: i32,
38    pub my_stock_qot_right: i32,
39    pub jp_stock_qot_right: i32,
40    pub cc_qot_right: i32,
41    /// CMD6024 field 63: dedicated digital-currency futures entitlement.
42    pub has_digital_future_auth: bool,
43    pub cc_pt_orderbook_qot_right: i32,
44    pub event_contract_qot_right: i32,
45    pub event_contract_prohibited_category_ids: Vec<u32>,
46    pub us_lv2_arca_qot_right: bool,
47    pub us_lv2_nyse_qot_right: bool,
48    pub us_lv2_nasdaq_totalview_qot_right: bool,
49    pub sub_quota: i32,
50    pub history_kl_quota: i32,
51    /// v1.4.106 codex 1209 F4: HK option/future orderbook depth.
52    pub hk_option_orderbook_depth: Option<u32>,
53    pub hk_future_orderbook_depth: Option<u32>,
54}
55
56impl Default for QotRightData {
57    fn default() -> Self {
58        const UNKNOWN: i32 = 0;
59        Self {
60            hk_own_qot_right: UNKNOWN,
61            hk_qot_right: UNKNOWN,
62            us_own_qot_right: UNKNOWN,
63            us_qot_right: UNKNOWN,
64            api_us_qot_right: UNKNOWN,
65            has_us_utp: false,
66            has_us_overnight: false,
67            sh_qot_right: UNKNOWN,
68            sz_qot_right: UNKNOWN,
69            hk_option_qot_right: UNKNOWN,
70            hk_future_qot_right: UNKNOWN,
71            // Cold-start UNKNOWN must not pass US option permission gates; after
72            // backend refresh this is recomputed from `us_option_qot_right`.
73            has_us_option_qot_right: false,
74            us_option_qot_right: UNKNOWN,
75            us_index_qot_right: UNKNOWN,
76            us_otc_qot_right: UNKNOWN,
77            us_cme_future_qot_right: UNKNOWN,
78            us_cbot_future_qot_right: UNKNOWN,
79            us_nymex_future_qot_right: UNKNOWN,
80            us_comex_future_qot_right: UNKNOWN,
81            us_cboe_future_qot_right: UNKNOWN,
82            sg_future_qot_right: UNKNOWN,
83            jp_future_qot_right: UNKNOWN,
84            sg_stock_qot_right: UNKNOWN,
85            my_stock_qot_right: UNKNOWN,
86            jp_stock_qot_right: UNKNOWN,
87            cc_qot_right: UNKNOWN,
88            has_digital_future_auth: false,
89            cc_pt_orderbook_qot_right: UNKNOWN,
90            event_contract_qot_right: UNKNOWN,
91            event_contract_prohibited_category_ids: Vec::new(),
92            us_lv2_arca_qot_right: false,
93            us_lv2_nyse_qot_right: false,
94            us_lv2_nasdaq_totalview_qot_right: false,
95            sub_quota: QOT_API_QUOTA_DEFAULT,
96            history_kl_quota: QOT_API_QUOTA_DEFAULT,
97            hk_option_orderbook_depth: None,
98            hk_future_orderbook_depth: None,
99        }
100    }
101}
102
103#[derive(Debug, Clone)]
104pub struct QotRightRefreshReport {
105    pub changed: bool,
106    pub reconnect_quote_types: Vec<i32>,
107    pub has_set_qot_right_before: bool,
108    pub has_us_utp_changed: bool,
109    pub quota_data_changed: bool,
110    pub quota_projection_available: bool,
111    pub backend_generation: u64,
112    pub route_publication_epoch: u64,
113    pub request_is_highest: bool,
114    pub force_resubscribe: bool,
115    pub resubscribe_scope: futu_domain_qot_right::QotRightResubscribeScope,
116    pub deferred_reconnect_token: Option<QotRightReconnectToken>,
117    pub deferred_reconnect_blocked: bool,
118    pub fresh_at_ms: i64,
119    pub decoded_fields: Vec<&'static str>,
120    pub freshness_after: QotRightFreshness,
121    pub login_epoch: u64,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct QotRightBackendApplyOutcome {
126    pub public_rights_changed: bool,
127    pub reconnect_quote_types: Vec<i32>,
128    pub has_set_qot_right_before: bool,
129    pub has_us_utp_changed: bool,
130    pub quota_data_changed: bool,
131    pub quota_projection_available: bool,
132}
133
134impl QotRightBackendApplyOutcome {
135    #[must_use]
136    pub fn cpp_reconnect_rights_changed(&self) -> bool {
137        !self.reconnect_quote_types.is_empty()
138    }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct QotRightFullApplyFacts {
143    pub expected_generation: u64,
144    /// Exact Platform route publication that produced the full entitlement
145    /// response. `None` is reserved for local/unit fixtures and cannot
146    /// authorize route-derived backend requests.
147    pub source_publication_epoch: Option<u64>,
148    pub is_highest: bool,
149    pub force_resub: bool,
150    pub pushed_notify_version: Option<u64>,
151}
152
153#[must_use]
154pub fn has_set_qot_right_like_cpp(data: &QotRightData) -> bool {
155    // Ref: NNData_Qot_Right.cpp:733-760. This is the exact 23-field OR;
156    // UTP, orderbook depth, quota, venue flags and crypto PT are excluded.
157    [
158        data.hk_own_qot_right,
159        data.hk_option_qot_right,
160        data.hk_qot_right,
161        data.us_qot_right,
162        data.us_own_qot_right,
163        data.sh_qot_right,
164        data.sz_qot_right,
165        data.hk_future_qot_right,
166        data.us_option_qot_right,
167        data.us_cme_future_qot_right,
168        data.us_cbot_future_qot_right,
169        data.us_nymex_future_qot_right,
170        data.us_comex_future_qot_right,
171        data.us_cboe_future_qot_right,
172        data.us_index_qot_right,
173        data.us_otc_qot_right,
174        data.sg_future_qot_right,
175        data.sg_stock_qot_right,
176        data.jp_future_qot_right,
177        data.my_stock_qot_right,
178        data.jp_stock_qot_right,
179        data.cc_qot_right,
180        data.event_contract_qot_right,
181    ]
182    .into_iter()
183    .any(|right| right != QOT_RIGHT_UNKNOWN)
184}
185
186#[derive(Debug, Clone)]
187pub struct QotRightStateMeta {
188    pub freshness: QotRightFreshness,
189    pub user_id: Option<u64>,
190    pub login_epoch: u64,
191    pub backend_generation: u64,
192    pub source_publication_epoch: Option<u64>,
193    pub last_refresh_at_ms: i64,
194    pub last_pushed_quote_change_notify: Option<PushedQuoteChangeNotify>,
195    pub last_pushed_quote_change_notify_version: u64,
196    pub reconnect_lifecycle: QotRightReconnectLifecycle,
197    pub next_reconnect_token: u64,
198    /// C++ preloads EC category data on the first None/Unknown -> Level1
199    /// transition. The cache owns this one-shot handoff so the eventual
200    /// category worker cannot miss the transition between refreshes.
201    pub event_contract_category_preload_pending: bool,
202}
203
204impl Default for QotRightStateMeta {
205    fn default() -> Self {
206        Self {
207            freshness: QotRightFreshness::Unknown,
208            user_id: None,
209            login_epoch: 0,
210            backend_generation: 0,
211            source_publication_epoch: None,
212            last_refresh_at_ms: 0,
213            last_pushed_quote_change_notify: None,
214            last_pushed_quote_change_notify_version: 0,
215            reconnect_lifecycle: QotRightReconnectLifecycle::Idle,
216            next_reconnect_token: 1,
217            event_contract_category_preload_pending: false,
218        }
219    }
220}
221
222pub(super) fn now_ms() -> i64 {
223    use std::time::{SystemTime, UNIX_EPOCH};
224    SystemTime::now()
225        .duration_since(UNIX_EPOCH)
226        .map(|d| d.as_millis() as i64)
227        .unwrap_or(0)
228}
229
230pub(super) fn next_reconnect_token(meta: &mut QotRightStateMeta) -> QotRightReconnectToken {
231    let token = QotRightReconnectToken(meta.next_reconnect_token);
232    meta.next_reconnect_token = meta.next_reconnect_token.saturating_add(1);
233    token
234}