Skip to main content

futu_qot/
quote_rights.rs

1//! 用户行情权限 profile。
2//!
3//! 这个 module 是 CLI / REST / MCP 的共享投影层:`GetUserInfo` 保持 SDK
4//! 兼容语义,GUI 风格权限面板则从 daemon 内部权限 cache snapshot 生成。
5
6use 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}