1use std::sync::Arc;
7
8use futu_backend::conn::BackendConn;
9use futu_cache::qot_right::{QotRightBackendExtras, QotRightBackendUpdate, QotRightCache};
10
11use super::{CacheBundle, GatewayBridge};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum QotRightRequestMode {
15 HighestPriority,
16 WithPushedNotify,
17}
18
19#[derive(Debug, Clone)]
20pub struct QotRightRefreshErr {
21 pub stage: &'static str,
22 pub detail: String,
23}
24
25impl std::fmt::Display for QotRightRefreshErr {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(
28 f,
29 "qot_right refresh failed at {}: {}",
30 self.stage, self.detail
31 )
32 }
33}
34
35impl std::error::Error for QotRightRefreshErr {}
36
37pub use futu_cache::qot_right::QotRightRefreshReport;
38
39pub(super) fn spawn_startup_refresh(
40 backend: Arc<BackendConn>,
41 caches: CacheBundle,
42 epoch_uid: Option<u64>,
43) -> tokio::task::JoinHandle<()> {
44 caches.qot_right_cache.mark_pending();
45 caches.qot_right_cache.advance_login_epoch(1, epoch_uid);
47 tokio::spawn(async move {
48 if let Err(e) = GatewayBridge::request_qot_right_v2(
49 &backend,
50 &caches.qot_right_cache,
51 QotRightRequestMode::HighestPriority,
52 )
53 .await
54 {
55 tracing::warn!(error = %e, "startup qot_right refresh failed");
56 }
57 })
58}
59
60fn qot_right_quote_change_notify(
61 qot_right_cache: &QotRightCache,
62 mode: QotRightRequestMode,
63) -> Option<futu_backend::proto_internal::ftcmd6651_qta_auth_chg::QuoteChangeNotify> {
64 match mode {
65 QotRightRequestMode::HighestPriority => None,
66 QotRightRequestMode::WithPushedNotify => {
67 qot_right_cache.pushed_quote_change_notify().map(|pn| {
68 use futu_backend::proto_internal::ftcmd6651_qta_auth_chg as chg;
69 chg::QuoteChangeNotify {
70 quote_change_list: pn
71 .change_items
72 .iter()
73 .map(|&(qt, before, after)| chg::QuoteChangeItem {
74 quote_type: Some(qt),
75 quote_before_change: Some(before),
76 quote_after_change: Some(after),
77 clt_type: None,
78 ip_area: None,
79 })
80 .collect(),
81 }
82 })
83 }
84 }
85}
86
87fn encode_qot_right_request(
88 quote_change_notify: Option<
89 futu_backend::proto_internal::ftcmd6651_qta_auth_chg::QuoteChangeNotify,
90 >,
91) -> Vec<u8> {
92 use futu_backend::proto_internal::qta_auth;
93
94 let req = qta_auth::QtaAuth6024Req {
95 req_hk_auth: None,
96 req_us_auth: None,
97 req_cn_auth: None,
98 device_type: None,
101 quote_change_notify,
102 };
103 prost::Message::encode_to_vec(&req)
104}
105
106fn decode_qot_right_response(
107 body_bytes: &[u8],
108) -> Result<futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp, String> {
109 crate::handlers_shared::decode_srpc_or_direct(body_bytes, is_valid_qta_auth_6024_rsp)
110 .map_err(|e| format!("decode failed (body_len={}): {}", body_bytes.len(), e))
111}
112
113fn is_valid_qta_auth_6024_rsp(r: &futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp) -> bool {
114 r.hk_qut_got_auth.is_some()
115 || r.us_qut_got_auth.is_some()
116 || r.cn_qut_got_auth.is_some()
117 || r.shanghai_qut_auth.is_some()
118 || r.shenzhen_qut_auth.is_some()
119 || r.hk_option_auth.is_some()
120 || r.hk_future_auth.is_some()
121 || r.hk_option_orderbook_depth.is_some()
122 || r.hk_future_orderbook_depth.is_some()
123 || r.us_option_flag.is_some()
124 || r.us_future_cme_cboe_auth.is_some()
125 || r.us_future_auth.is_some()
126 || r.us_lv2_arca_flag.is_some()
127 || r.us_lv2_nyse_flag.is_some()
128 || r.us_lv2_nasdaq_tv_flag.is_some()
129 || r.us_lv2_edg_flag.is_some()
130 || r.us_lv2_bzx_flag.is_some()
131 || r.us_index_dow_jones_flag.is_some()
132 || r.us_index_nasdaq_flag.is_some()
133 || r.us_index_standard_poor_flag.is_some()
134 || r.us_otc_deal_data_auth.is_some()
135 || r.us_otc_order_book_auth.is_some()
136 || r.sgx_future_auth.is_some()
137 || r.sgx_stock_auth.is_some()
138 || r.jp_future_auth.is_some()
139 || r.jp_stock_tse_auth.is_some()
140 || r.myx_auth.is_some()
141 || r.digital_currency_auth.is_some()
142 || r.digital_pt_orderbook_auth.is_some()
143 || r.open_api_auth.is_some()
144}
145
146fn qot_right_backend_extras_from_rsp(
147 r: &futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp,
148) -> QotRightBackendExtras {
149 QotRightBackendExtras {
150 us_lv2_flags: Some((
151 r.us_lv2_arca_flag,
152 r.us_lv2_nyse_flag,
153 r.us_lv2_nasdaq_tv_flag,
154 r.us_lv2_edg_flag,
155 r.us_lv2_bzx_flag,
156 )),
157 us_index_flags: Some((
158 r.us_index_dow_jones_flag,
159 r.us_index_nasdaq_flag,
160 r.us_index_standard_poor_flag,
161 )),
162 us_otc_auths: Some((r.us_otc_deal_data_auth, r.us_otc_order_book_auth)),
163 }
164}
165
166fn qot_right_backend_update_from_rsp(
167 r: &futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp,
168) -> QotRightBackendUpdate {
169 let (sub_limit, kl_limit) = r
170 .open_api_auth
171 .as_ref()
172 .map(|api_auth| (api_auth.sub_limit, api_auth.history_k_line_limit))
173 .unwrap_or((None, None));
174 let us_future_detail = r.us_future_auth.as_ref().and_then(|f| {
177 let detail = (
178 f.open_api_cme_auth,
179 f.open_api_cbot_auth,
180 f.open_api_nymex_auth,
181 f.open_api_comex_auth,
182 );
183 if detail.0.is_none() && detail.1.is_none() && detail.2.is_none() && detail.3.is_none() {
184 None
185 } else {
186 Some(detail)
187 }
188 });
189
190 let clear_missing_sg_future = Some(r.sgx_future_auth.unwrap_or(0));
195 let clear_missing_jp_future = Some(r.jp_future_auth.unwrap_or(0));
196 let clear_missing_sg_stock = Some(r.sgx_stock_auth.unwrap_or(0));
197 let clear_missing_my_stock = Some(r.myx_auth.as_ref().and_then(|m| m.myx_auth).unwrap_or(0));
198 let clear_missing_jp_stock = Some(r.jp_stock_tse_auth.unwrap_or(0));
199 let clear_missing_crypto = Some(r.digital_currency_auth.unwrap_or(0));
200 let clear_missing_crypto_pt_orderbook = Some(r.digital_pt_orderbook_auth.unwrap_or(0));
201
202 QotRightBackendUpdate {
203 hk_got: r.hk_qut_got_auth,
204 us_got: r.us_qut_got_auth,
205 cn_got: r.cn_qut_got_auth,
206 sh_auth: r.shanghai_qut_auth,
207 sz_auth: r.shenzhen_qut_auth,
208 hk_option: r.hk_option_auth,
209 hk_future: r.hk_future_auth,
210 us_option: r.us_option_flag,
211 us_future_cme_cboe: r.us_future_cme_cboe_auth,
212 us_future_detail,
213 sg_future: clear_missing_sg_future,
214 jp_future: clear_missing_jp_future,
215 sg_stock: clear_missing_sg_stock,
216 my_stock: clear_missing_my_stock,
217 jp_stock: clear_missing_jp_stock,
218 digital_currency_auth: clear_missing_crypto,
219 digital_pt_orderbook_auth: clear_missing_crypto_pt_orderbook,
220 sub_limit,
221 kl_limit,
222 extras: qot_right_backend_extras_from_rsp(r),
223 }
224}
225
226fn apply_qot_right_backend_response(
227 qot_right_cache: &QotRightCache,
228 rsp: &futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp,
229) {
230 qot_right_cache.update_from_backend(qot_right_backend_update_from_rsp(rsp));
231 qot_right_cache
232 .set_orderbook_depths(rsp.hk_option_orderbook_depth, rsp.hk_future_orderbook_depth);
233}
234
235fn collect_decoded_fields(
236 r: &futu_backend::proto_internal::qta_auth::QtaAuth6024Rsp,
237) -> Vec<&'static str> {
238 let mut v: Vec<&'static str> = Vec::new();
239 if r.hk_qut_got_auth.is_some() {
240 v.push("hk_qut_got_auth");
241 }
242 if r.us_qut_got_auth.is_some() {
243 v.push("us_qut_got_auth");
244 }
245 if r.cn_qut_got_auth.is_some() {
246 v.push("cn_qut_got_auth");
247 }
248 if r.shanghai_qut_auth.is_some() {
249 v.push("shanghai_qut_auth");
250 }
251 if r.shenzhen_qut_auth.is_some() {
252 v.push("shenzhen_qut_auth");
253 }
254 if r.hk_option_auth.is_some() {
255 v.push("hk_option_auth");
256 }
257 if r.hk_future_auth.is_some() {
258 v.push("hk_future_auth");
259 }
260 if r.hk_option_orderbook_depth.is_some() {
261 v.push("hk_option_orderbook_depth");
262 }
263 if r.hk_future_orderbook_depth.is_some() {
264 v.push("hk_future_orderbook_depth");
265 }
266 if r.us_option_flag.is_some() {
267 v.push("us_option_flag");
268 }
269 if r.us_future_cme_cboe_auth.is_some() {
270 v.push("us_future_cme_cboe_auth");
271 }
272 if r.us_future_auth.is_some() {
273 v.push("us_future_auth");
274 }
275 if r.us_lv2_arca_flag.is_some() {
276 v.push("us_lv2_arca_flag");
277 }
278 if r.us_lv2_nyse_flag.is_some() {
279 v.push("us_lv2_nyse_flag");
280 }
281 if r.us_lv2_nasdaq_tv_flag.is_some() {
282 v.push("us_lv2_nasdaq_tv_flag");
283 }
284 if r.us_lv2_edg_flag.is_some() {
285 v.push("us_lv2_edg_flag");
286 }
287 if r.us_lv2_bzx_flag.is_some() {
288 v.push("us_lv2_bzx_flag");
289 }
290 if r.us_index_dow_jones_flag.is_some() {
291 v.push("us_index_dow_jones_flag");
292 }
293 if r.us_index_nasdaq_flag.is_some() {
294 v.push("us_index_nasdaq_flag");
295 }
296 if r.us_index_standard_poor_flag.is_some() {
297 v.push("us_index_standard_poor_flag");
298 }
299 if r.us_otc_deal_data_auth.is_some() {
300 v.push("us_otc_deal_data_auth");
301 }
302 if r.us_otc_order_book_auth.is_some() {
303 v.push("us_otc_order_book_auth");
304 }
305 if r.sgx_future_auth.is_some() {
306 v.push("sgx_future_auth");
307 }
308 if r.sgx_stock_auth.is_some() {
309 v.push("sgx_stock_auth");
310 }
311 if r.jp_future_auth.is_some() {
312 v.push("jp_future_auth");
313 }
314 if r.jp_stock_tse_auth.is_some() {
315 v.push("jp_stock_tse_auth");
316 }
317 if r.myx_auth.is_some() {
318 v.push("myx_auth");
319 }
320 if r.digital_currency_auth.is_some() {
321 v.push("digital_currency_auth");
322 }
323 if r.digital_pt_orderbook_auth.is_some() {
324 v.push("digital_pt_orderbook_auth");
325 }
326 if r.open_api_auth.is_some() {
327 v.push("open_api_auth");
328 }
329 v
330}
331
332impl GatewayBridge {
333 pub async fn request_qot_right(
339 backend: &futu_backend::conn::BackendConn,
340 qot_right_cache: &QotRightCache,
341 ) {
342 tracing::info!("CMD6024: requesting qot right...");
343
344 let body = encode_qot_right_request(None);
345
346 match backend.request(6032, body).await {
349 Ok(frame) => {
350 let body_bytes = frame.body.as_ref();
351
352 let rsp = match decode_qot_right_response(body_bytes) {
356 Ok(r) => r,
357 Err(e) => {
358 tracing::warn!(
359 error = %e,
360 body_len = body_bytes.len(),
361 "CMD6024 SRPC decode failed; preserving existing qot_right cache"
362 );
363 return;
364 }
365 };
366
367 let open_api_limits = rsp
368 .open_api_auth
369 .as_ref()
370 .map(|api_auth| (api_auth.sub_limit, api_auth.history_k_line_limit))
371 .unwrap_or((None, None));
372 apply_qot_right_backend_response(qot_right_cache, &rsp);
373
374 tracing::info!(
375 hk_got = rsp.hk_qut_got_auth.unwrap_or(0),
376 us_got = rsp.us_qut_got_auth.unwrap_or(0),
377 cn_got = rsp.cn_qut_got_auth.unwrap_or(0),
378 raw_us_lv2_arca = ?rsp.us_lv2_arca_flag,
379 raw_us_lv2_nyse = ?rsp.us_lv2_nyse_flag,
380 raw_us_lv2_nasdaq_tv = ?rsp.us_lv2_nasdaq_tv_flag,
381 raw_us_lv2_edg = ?rsp.us_lv2_edg_flag,
382 raw_us_lv2_bzx = ?rsp.us_lv2_bzx_flag,
383 raw_us_index_dow_jones = ?rsp.us_index_dow_jones_flag,
384 raw_us_index_nasdaq = ?rsp.us_index_nasdaq_flag,
385 raw_us_index_standard_poor = ?rsp.us_index_standard_poor_flag,
386 raw_us_otc_deal_data = ?rsp.us_otc_deal_data_auth,
387 raw_us_otc_order_book = ?rsp.us_otc_order_book_auth,
388 raw_us_future_cme_cboe = ?rsp.us_future_cme_cboe_auth,
389 raw_sgx_stock_auth = ?rsp.sgx_stock_auth,
390 raw_myx_auth = ?rsp.myx_auth.as_ref().and_then(|m| m.myx_auth),
391 raw_jp_stock_tse_auth = ?rsp.jp_stock_tse_auth,
392 raw_digital_currency_auth = ?rsp.digital_currency_auth,
393 raw_digital_pt_orderbook_auth = ?rsp.digital_pt_orderbook_auth,
394 hk_future = rsp.hk_future_auth.unwrap_or(0),
395 hk_option = rsp.hk_option_auth.unwrap_or(0),
396 sub_limit = open_api_limits.0.unwrap_or(0),
397 kl_limit = open_api_limits.1.unwrap_or(0),
398 "CMD6024 qot right updated"
399 );
400 }
401 Err(e) => {
402 tracing::warn!(error = %e, "CMD6024 request failed (using defaults)");
403 }
404 }
405 }
406
407 pub async fn request_qot_right_v2(
408 backend: &futu_backend::conn::BackendConn,
409 qot_right_cache: &QotRightCache,
410 mode: QotRightRequestMode,
411 ) -> Result<QotRightRefreshReport, QotRightRefreshErr> {
412 qot_right_cache.mark_pending();
413
414 let quote_change_notify = qot_right_quote_change_notify(qot_right_cache, mode);
415
416 let mode_str = match mode {
417 QotRightRequestMode::HighestPriority => "HighestPriority",
418 QotRightRequestMode::WithPushedNotify => "WithPushedNotify",
419 };
420 tracing::info!(
421 mode = mode_str,
422 has_pushed_notify = quote_change_notify.is_some(),
423 "CMD6032: requesting qot right (v2)..."
424 );
425
426 let body = encode_qot_right_request(quote_change_notify);
427
428 let frame = match backend.request(6032, body).await {
429 Ok(f) => f,
430 Err(e) => {
431 let err_msg = format!("backend request failed: {e}");
432 qot_right_cache.mark_failed(err_msg.clone());
433 return Err(QotRightRefreshErr {
434 stage: "request",
435 detail: err_msg,
436 });
437 }
438 };
439
440 let body_bytes = frame.body.as_ref();
441
442 let rsp = match decode_qot_right_response(body_bytes) {
443 Ok(r) => r,
444 Err(err_msg) => {
445 qot_right_cache.mark_failed(err_msg.clone());
446 return Err(QotRightRefreshErr {
447 stage: "decode",
448 detail: err_msg,
449 });
450 }
451 };
452
453 let raw_hk_future_auth = rsp.hk_future_auth;
454 let raw_hk_option_auth = rsp.hk_option_auth;
455 let raw_hk_future_orderbook_depth = rsp.hk_future_orderbook_depth;
456 let raw_hk_option_orderbook_depth = rsp.hk_option_orderbook_depth;
457 let raw_us_lv2_arca = rsp.us_lv2_arca_flag;
458 let raw_us_lv2_nyse = rsp.us_lv2_nyse_flag;
459 let raw_us_lv2_nasdaq_tv = rsp.us_lv2_nasdaq_tv_flag;
460 let raw_us_lv2_edg = rsp.us_lv2_edg_flag;
461 let raw_us_lv2_bzx = rsp.us_lv2_bzx_flag;
462 let raw_us_index_dow_jones = rsp.us_index_dow_jones_flag;
463 let raw_us_index_nasdaq = rsp.us_index_nasdaq_flag;
464 let raw_us_index_standard_poor = rsp.us_index_standard_poor_flag;
465 let raw_us_otc_deal_data = rsp.us_otc_deal_data_auth;
466 let raw_us_otc_order_book = rsp.us_otc_order_book_auth;
467 let raw_us_future_cme_cboe = rsp.us_future_cme_cboe_auth;
468 let raw_sgx_stock_auth = rsp.sgx_stock_auth;
469 let raw_myx_auth = rsp.myx_auth.as_ref().and_then(|m| m.myx_auth);
470 let raw_jp_stock_tse_auth = rsp.jp_stock_tse_auth;
471 let raw_digital_currency_auth = rsp.digital_currency_auth;
472 let raw_digital_pt_orderbook_auth = rsp.digital_pt_orderbook_auth;
473 apply_qot_right_backend_response(qot_right_cache, &rsp);
474
475 let decoded_fields = collect_decoded_fields(&rsp);
476 let meta = qot_right_cache.meta_snapshot();
477 let data = qot_right_cache.get();
478 tracing::info!(
479 mode = mode_str,
480 decoded_fields = ?decoded_fields,
481 raw_hk_future_auth = ?raw_hk_future_auth,
482 raw_hk_option_auth = ?raw_hk_option_auth,
483 raw_hk_future_orderbook_depth = ?raw_hk_future_orderbook_depth,
484 raw_hk_option_orderbook_depth = ?raw_hk_option_orderbook_depth,
485 raw_us_lv2_arca = ?raw_us_lv2_arca,
486 raw_us_lv2_nyse = ?raw_us_lv2_nyse,
487 raw_us_lv2_nasdaq_tv = ?raw_us_lv2_nasdaq_tv,
488 raw_us_lv2_edg = ?raw_us_lv2_edg,
489 raw_us_lv2_bzx = ?raw_us_lv2_bzx,
490 raw_us_index_dow_jones = ?raw_us_index_dow_jones,
491 raw_us_index_nasdaq = ?raw_us_index_nasdaq,
492 raw_us_index_standard_poor = ?raw_us_index_standard_poor,
493 raw_us_otc_deal_data = ?raw_us_otc_deal_data,
494 raw_us_otc_order_book = ?raw_us_otc_order_book,
495 raw_us_future_cme_cboe = ?raw_us_future_cme_cboe,
496 raw_sgx_stock_auth = ?raw_sgx_stock_auth,
497 raw_myx_auth = ?raw_myx_auth,
498 raw_jp_stock_tse_auth = ?raw_jp_stock_tse_auth,
499 raw_digital_currency_auth = ?raw_digital_currency_auth,
500 raw_digital_pt_orderbook_auth = ?raw_digital_pt_orderbook_auth,
501 hk_future_qot_right = data.hk_future_qot_right,
502 hk_option_qot_right = data.hk_option_qot_right,
503 us_qot_right = data.us_qot_right,
504 us_index_qot_right = data.us_index_qot_right,
505 us_otc_qot_right = data.us_otc_qot_right,
506 us_cboe_future_qot_right = data.us_cboe_future_qot_right,
507 sg_stock_qot_right = data.sg_stock_qot_right,
508 my_stock_qot_right = data.my_stock_qot_right,
509 jp_stock_qot_right = data.jp_stock_qot_right,
510 cc_qot_right = data.cc_qot_right,
511 cc_pt_orderbook_qot_right = data.cc_pt_orderbook_qot_right,
512 hk_future_orderbook_depth = ?data.hk_future_orderbook_depth,
513 hk_option_orderbook_depth = ?data.hk_option_orderbook_depth,
514 freshness = ?meta.freshness,
515 "CMD6032: qot right updated (API-facing cache values)"
516 );
517
518 Ok(QotRightRefreshReport {
519 changed: true,
520 fresh_at_ms: meta.last_refresh_at_ms,
521 decoded_fields,
522 freshness_after: meta.freshness,
523 login_epoch: meta.login_epoch,
524 })
525 }
526}