1use 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#[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 #[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 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;