Skip to main content

futu_core/
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 crate::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    pub event_contract_qot_right: Option<i32>,
211}
212
213pub fn profile_from_snapshot(s: QuoteRightsSnapshot) -> QuoteRightsProfile {
214    let mut items = Vec::with_capacity(21);
215    push_item(
216        &mut items,
217        "香港市场",
218        "股票",
219        s.hk_qot_right,
220        label_standard,
221    );
222    push_item(
223        &mut items,
224        "香港市场",
225        "期权",
226        s.hk_option_qot_right,
227        label_standard,
228    );
229    push_item(
230        &mut items,
231        "香港市场",
232        "期货",
233        s.hk_future_qot_right,
234        label_standard,
235    );
236    push_item(
237        &mut items,
238        "美国市场",
239        "股票",
240        s.us_qot_right,
241        label_us_stock,
242    );
243    push_item(
244        &mut items,
245        "美国市场",
246        "指数",
247        s.us_index_qot_right,
248        label_standard,
249    );
250    push_item(
251        &mut items,
252        "美国市场",
253        "期权",
254        s.us_option_qot_right,
255        label_standard,
256    );
257    push_item(
258        &mut items,
259        "美国市场",
260        "CME",
261        s.us_cme_future_qot_right,
262        label_standard,
263    );
264    push_item(
265        &mut items,
266        "美国市场",
267        "CBOT",
268        s.us_cbot_future_qot_right,
269        label_standard,
270    );
271    push_item(
272        &mut items,
273        "美国市场",
274        "NYMEX",
275        s.us_nymex_future_qot_right,
276        label_standard,
277    );
278    push_item(
279        &mut items,
280        "美国市场",
281        "COMEX",
282        s.us_comex_future_qot_right,
283        label_standard,
284    );
285    push_item(
286        &mut items,
287        "美国市场",
288        "CBOE",
289        s.us_cboe_future_qot_right,
290        label_standard,
291    );
292    push_item(
293        &mut items,
294        "美国市场",
295        "OTC",
296        s.us_otc_qot_right,
297        label_standard,
298    );
299    push_item(
300        &mut items,
301        "A股市场",
302        "上证",
303        s.sh_qot_right,
304        label_standard,
305    );
306    push_item(
307        &mut items,
308        "A股市场",
309        "深证",
310        s.sz_qot_right,
311        label_standard,
312    );
313    push_item(
314        &mut items,
315        "新加坡市场",
316        "股票",
317        s.sg_stock_qot_right,
318        label_standard,
319    );
320    push_item(
321        &mut items,
322        "新加坡市场",
323        "期货",
324        s.sg_future_qot_right,
325        label_standard,
326    );
327    push_item(
328        &mut items,
329        "马来西亚市场",
330        "股票",
331        s.my_stock_qot_right,
332        label_standard,
333    );
334    push_item(
335        &mut items,
336        "日本市场",
337        "股票",
338        s.jp_stock_qot_right,
339        label_standard,
340    );
341    push_item(
342        &mut items,
343        "日本市场",
344        "期货",
345        s.jp_future_qot_right,
346        label_standard,
347    );
348    push_item(
349        &mut items,
350        "加密货币市场",
351        "加密货币",
352        s.cc_qot_right,
353        label_standard,
354    );
355    push_item(
356        &mut items,
357        "预测市场",
358        "事件合约",
359        s.event_contract_qot_right,
360        label_standard,
361    );
362
363    QuoteRightsProfile {
364        nick_name: s.nick_name,
365        user_id: s.user_id,
366        user_attribution: s.user_attribution,
367        user_attribution_region: user_attribution_region(s.user_attribution)
368            .map(ToString::to_string),
369        ret_msg: s.ret_msg,
370        quota: QuoteRightsQuota {
371            subscribe_total: s.sub_quota,
372            history_kl_total: s.history_kl_quota,
373        },
374        items,
375    }
376}
377
378fn push_item(
379    items: &mut Vec<QuoteRightItem>,
380    market: &str,
381    category: &str,
382    raw: Option<i32>,
383    label_fn: fn(Option<i32>) -> String,
384) {
385    items.push(QuoteRightItem {
386        market: market.to_string(),
387        category: category.to_string(),
388        raw,
389        label: label_fn(raw),
390    });
391}
392
393fn label_standard(raw: Option<i32>) -> String {
394    match raw {
395        None | Some(0) => "未知".to_string(),
396        Some(1) => "BMP".to_string(),
397        Some(2) => "LV1".to_string(),
398        Some(3) => "LV2".to_string(),
399        Some(4) => "SF".to_string(),
400        Some(5) => "无权限".to_string(),
401        Some(6) => "LV3".to_string(),
402        Some(v) => format!("未知({v})"),
403    }
404}
405
406fn label_us_stock(raw: Option<i32>) -> String {
407    match raw {
408        Some(3) => "LV3".to_string(),
409        other => label_standard(other),
410    }
411}
412
413fn user_attribution_region(raw: Option<i32>) -> Option<&'static str> {
414    match raw {
415        Some(1) => Some("CN"),
416        Some(2) => Some("US"),
417        Some(3) => Some("SG"),
418        Some(4) => Some("AU"),
419        Some(5) => Some("JP"),
420        Some(6) => Some("HK"),
421        _ => None,
422    }
423}