1use serde::{Deserialize, Serialize};
7
8pub use futu_core::diagnostic::DiagnosticDecision as QuoteCapabilityDecision;
9
10pub const SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE: &str = "sys.get_quote_rights_profile";
11pub const SYS_QUERY_GET_QUOTE_CAPABILITY: &str = "sys.get_quote_capability";
12pub const TEST_CMD_GET_QUOTE_RIGHTS_PROFILE: &str = "get_quote_rights_profile";
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct QuoteRightsProfile {
16 pub nick_name: Option<String>,
17 pub user_id: Option<i64>,
18 pub user_attribution: Option<i32>,
19 pub user_attribution_region: Option<String>,
20 pub ret_msg: Option<String>,
21 pub quota: QuoteRightsQuota,
22 pub items: Vec<QuoteRightItem>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct QuoteRightsQuota {
27 pub subscribe_total: Option<i32>,
28 pub history_kl_total: Option<i32>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct QuoteRightItem {
33 pub market: String,
34 pub category: String,
35 pub raw: Option<i32>,
36 pub label: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct QuoteCapabilityReport {
41 pub security: QuoteCapabilitySecurity,
42 pub source: QuoteCapabilitySource,
43 pub raw_rights: QuoteCapabilityRawRights,
44 pub orderbook: QuoteCapabilityOrderBook,
45 pub snapshot: QuoteCapabilitySnapshot,
46 pub crypto: QuoteCapabilityCrypto,
47 pub lv2_subs: Vec<QuoteCapabilityLv2Sub>,
48 pub decisions: Vec<QuoteCapabilityDecision>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct QuoteCapabilitySecurity {
53 pub symbol: String,
54 pub market: i32,
55 pub code: String,
56 pub stock_id: u64,
57 pub mkt_id: u32,
58 pub sec_type: i32,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct QuoteCapabilitySource {
63 pub static_cache: bool,
64 pub static_source: String,
65 pub qot_right_freshness: String,
66 pub ret_msg: Option<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct QuoteCapabilityRawRights {
71 pub hk_stock: i32,
72 pub us_stock: i32,
73 pub us_stock_internal: i32,
74 pub us_lv2_arca: bool,
75 pub us_lv2_nyse: bool,
76 pub us_lv2_nasdaq_totalview: bool,
77 pub sg_stock: i32,
78 pub my_stock: i32,
79 pub jp_stock: i32,
80 pub crypto: i32,
81 pub crypto_pt_orderbook: i32,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct QuoteCapabilityOrderBook {
86 #[serde(default)]
87 pub max_depth: Option<usize>,
88 #[serde(default)]
89 pub base_max_depth: usize,
90 pub read_uses_requested_count: bool,
91 pub requires_accepted_lv2_push: bool,
92 #[serde(default)]
93 pub depth_policy: String,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum QuoteCapabilityOrderBookDepthPolicy {
99 RequestedCount { base_max_depth: usize },
100 FixedMaxDepth { max_depth: usize },
101 Unavailable { max_depth: usize },
102}
103
104impl QuoteCapabilityOrderBookDepthPolicy {
105 #[must_use]
106 pub fn from_raw(read_uses_requested_count: bool, max_depth: usize) -> Self {
107 if read_uses_requested_count {
108 Self::RequestedCount {
109 base_max_depth: max_depth,
110 }
111 } else if max_depth > 0 {
112 Self::FixedMaxDepth { max_depth }
113 } else {
114 Self::Unavailable { max_depth }
115 }
116 }
117
118 #[must_use]
119 pub fn as_str(self) -> &'static str {
120 match self {
121 Self::RequestedCount { .. } => "requested_count",
122 Self::FixedMaxDepth { .. } => "fixed_max_depth",
123 Self::Unavailable { .. } => "unavailable",
124 }
125 }
126
127 #[must_use]
128 pub fn report_max_depth(self) -> Option<usize> {
129 match self {
130 Self::RequestedCount { .. } => None,
131 Self::FixedMaxDepth { max_depth } | Self::Unavailable { max_depth } => Some(max_depth),
132 }
133 }
134
135 #[must_use]
136 pub fn base_max_depth(self) -> usize {
137 match self {
138 Self::RequestedCount { base_max_depth } => base_max_depth,
139 Self::FixedMaxDepth { max_depth } | Self::Unavailable { max_depth } => max_depth,
140 }
141 }
142
143 #[must_use]
144 pub fn decision_reason(self) -> String {
145 match self {
146 Self::RequestedCount { base_max_depth } => {
147 format!("depth_policy=requested_count base_max_depth={base_max_depth}")
148 }
149 Self::FixedMaxDepth { max_depth } => {
150 format!("depth_policy=fixed_max_depth max_depth={max_depth}")
151 }
152 Self::Unavailable { max_depth } => {
153 format!("depth_policy=unavailable max_depth={max_depth}")
154 }
155 }
156 }
157
158 #[must_use]
159 pub fn is_available(self) -> bool {
160 !matches!(self, Self::Unavailable { .. })
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct QuoteCapabilitySnapshot {
166 pub masks_hk_bmp_bid_ask: bool,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170pub struct QuoteCapabilityCrypto {
171 pub has_pt_orderbook_level1: bool,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct QuoteCapabilityLv2Sub {
176 pub name: String,
177 pub lv2_type: u32,
178 pub level: u32,
179 pub prob2_v2: bool,
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
183pub struct QuoteRightsSnapshot {
184 pub nick_name: Option<String>,
185 pub user_id: Option<i64>,
186 pub user_attribution: Option<i32>,
187 pub ret_msg: Option<String>,
188 pub sub_quota: Option<i32>,
189 pub history_kl_quota: Option<i32>,
190 pub hk_qot_right: Option<i32>,
191 pub hk_option_qot_right: Option<i32>,
192 pub hk_future_qot_right: Option<i32>,
193 pub us_qot_right: Option<i32>,
194 pub us_index_qot_right: Option<i32>,
195 pub us_option_qot_right: Option<i32>,
196 pub us_cme_future_qot_right: Option<i32>,
197 pub us_cbot_future_qot_right: Option<i32>,
198 pub us_nymex_future_qot_right: Option<i32>,
199 pub us_comex_future_qot_right: Option<i32>,
200 pub us_cboe_future_qot_right: Option<i32>,
201 pub us_otc_qot_right: Option<i32>,
202 pub sh_qot_right: Option<i32>,
203 pub sz_qot_right: Option<i32>,
204 pub sg_future_qot_right: Option<i32>,
205 pub sg_stock_qot_right: Option<i32>,
206 pub my_stock_qot_right: Option<i32>,
207 pub jp_future_qot_right: Option<i32>,
208 pub jp_stock_qot_right: Option<i32>,
209 pub cc_qot_right: Option<i32>,
210}
211
212pub fn profile_from_get_user_info(
213 resp: &futu_proto::get_user_info::Response,
214) -> Result<QuoteRightsProfile, String> {
215 if resp.ret_type != 0 {
216 return Err(format!(
217 "get_user_info ret_type={} msg={}",
218 resp.ret_type,
219 resp.ret_msg.as_deref().unwrap_or("")
220 ));
221 }
222 let s = resp.s2c.as_ref().ok_or_else(|| {
223 format!(
224 "get_user_info ret_type=0 but missing s2c msg={}",
225 resp.ret_msg.as_deref().unwrap_or("")
226 )
227 })?;
228
229 Ok(profile_from_snapshot(QuoteRightsSnapshot {
230 nick_name: s.nick_name.clone(),
231 user_id: s.user_id,
232 user_attribution: s.user_attribution,
233 ret_msg: resp.ret_msg.clone(),
234 sub_quota: s.sub_quota,
235 history_kl_quota: s.history_kl_quota,
236 hk_qot_right: s.hk_qot_right,
237 hk_option_qot_right: s.hk_option_qot_right,
238 hk_future_qot_right: s.hk_future_qot_right,
239 us_qot_right: s.us_qot_right,
240 us_index_qot_right: s.us_index_qot_right,
241 us_option_qot_right: s.us_option_qot_right,
242 us_cme_future_qot_right: s.us_cme_future_qot_right,
243 us_cbot_future_qot_right: s.us_cbot_future_qot_right,
244 us_nymex_future_qot_right: s.us_nymex_future_qot_right,
245 us_comex_future_qot_right: s.us_comex_future_qot_right,
246 us_cboe_future_qot_right: s.us_cboe_future_qot_right,
247 us_otc_qot_right: s.us_otc_qot_right,
248 sh_qot_right: s.sh_qot_right,
249 sz_qot_right: s.sz_qot_right,
250 sg_future_qot_right: s.sg_future_qot_right,
251 sg_stock_qot_right: s.sg_stock_qot_right,
252 my_stock_qot_right: s.my_stock_qot_right,
253 jp_future_qot_right: s.jp_future_qot_right,
254 jp_stock_qot_right: s.jp_stock_qot_right,
255 cc_qot_right: s.cc_qot_right,
256 }))
257}
258
259pub fn profile_from_snapshot(s: QuoteRightsSnapshot) -> QuoteRightsProfile {
260 let mut items = Vec::with_capacity(21);
261 push_item(
262 &mut items,
263 "香港市场",
264 "股票",
265 s.hk_qot_right,
266 label_standard,
267 );
268 push_item(
269 &mut items,
270 "香港市场",
271 "期权",
272 s.hk_option_qot_right,
273 label_standard,
274 );
275 push_item(
276 &mut items,
277 "香港市场",
278 "期货",
279 s.hk_future_qot_right,
280 label_standard,
281 );
282 push_item(
283 &mut items,
284 "美国市场",
285 "股票",
286 s.us_qot_right,
287 label_us_stock,
288 );
289 push_item(
290 &mut items,
291 "美国市场",
292 "指数",
293 s.us_index_qot_right,
294 label_standard,
295 );
296 push_item(
297 &mut items,
298 "美国市场",
299 "期权",
300 s.us_option_qot_right,
301 label_standard,
302 );
303 push_item(
304 &mut items,
305 "美国市场",
306 "CME",
307 s.us_cme_future_qot_right,
308 label_standard,
309 );
310 push_item(
311 &mut items,
312 "美国市场",
313 "CBOT",
314 s.us_cbot_future_qot_right,
315 label_standard,
316 );
317 push_item(
318 &mut items,
319 "美国市场",
320 "NYMEX",
321 s.us_nymex_future_qot_right,
322 label_standard,
323 );
324 push_item(
325 &mut items,
326 "美国市场",
327 "COMEX",
328 s.us_comex_future_qot_right,
329 label_standard,
330 );
331 push_item(
332 &mut items,
333 "美国市场",
334 "CBOE",
335 s.us_cboe_future_qot_right,
336 label_standard,
337 );
338 push_item(
339 &mut items,
340 "美国市场",
341 "OTC",
342 s.us_otc_qot_right,
343 label_standard,
344 );
345 push_item(
346 &mut items,
347 "A股市场",
348 "上证",
349 s.sh_qot_right,
350 label_standard,
351 );
352 push_item(
353 &mut items,
354 "A股市场",
355 "深证",
356 s.sz_qot_right,
357 label_standard,
358 );
359 push_item(
360 &mut items,
361 "新加坡市场",
362 "股票",
363 s.sg_stock_qot_right,
364 label_standard,
365 );
366 push_item(
367 &mut items,
368 "新加坡市场",
369 "期货",
370 s.sg_future_qot_right,
371 label_standard,
372 );
373 push_item(
374 &mut items,
375 "马来西亚市场",
376 "股票",
377 s.my_stock_qot_right,
378 label_standard,
379 );
380 push_item(
381 &mut items,
382 "日本市场",
383 "股票",
384 s.jp_stock_qot_right,
385 label_standard,
386 );
387 push_item(
388 &mut items,
389 "日本市场",
390 "期货",
391 s.jp_future_qot_right,
392 label_standard,
393 );
394 push_item(
395 &mut items,
396 "加密货币市场",
397 "加密货币",
398 s.cc_qot_right,
399 label_standard,
400 );
401
402 QuoteRightsProfile {
403 nick_name: s.nick_name,
404 user_id: s.user_id,
405 user_attribution: s.user_attribution,
406 user_attribution_region: user_attribution_region(s.user_attribution)
407 .map(ToString::to_string),
408 ret_msg: s.ret_msg,
409 quota: QuoteRightsQuota {
410 subscribe_total: s.sub_quota,
411 history_kl_total: s.history_kl_quota,
412 },
413 items,
414 }
415}
416
417fn push_item(
418 items: &mut Vec<QuoteRightItem>,
419 market: &str,
420 category: &str,
421 raw: Option<i32>,
422 label_fn: fn(Option<i32>) -> String,
423) {
424 items.push(QuoteRightItem {
425 market: market.to_string(),
426 category: category.to_string(),
427 raw,
428 label: label_fn(raw),
429 });
430}
431
432fn label_standard(raw: Option<i32>) -> String {
433 match raw {
434 None | Some(0) => "未知".to_string(),
435 Some(1) => "BMP".to_string(),
436 Some(2) => "LV1".to_string(),
437 Some(3) => "LV2".to_string(),
438 Some(4) => "SF".to_string(),
439 Some(5) => "无权限".to_string(),
440 Some(6) => "LV3".to_string(),
441 Some(v) => format!("未知({v})"),
442 }
443}
444
445fn label_us_stock(raw: Option<i32>) -> String {
446 match raw {
447 Some(3) => "LV3".to_string(),
448 other => label_standard(other),
449 }
450}
451
452fn user_attribution_region(raw: Option<i32>) -> Option<&'static str> {
453 match raw {
454 Some(1) => Some("CN"),
455 Some(2) => Some("US"),
456 Some(3) => Some("SG"),
457 Some(4) => Some("AU"),
458 Some(5) => Some("JP"),
459 Some(6) => Some("HK"),
460 _ => None,
461 }
462}