Skip to main content

futu_qot/
right_gate.rs

1//! QOT subscription permission decisions.
2//!
3//! This module intentionally keeps only plain domain inputs. Gateway/cache
4//! adapters turn backend right data and static security metadata into these
5//! structs, so REST/gRPC/raw-WS/MCP can converge on one permission matrix.
6
7pub const SECURITY_TYPE_DRVT: i32 = 8;
8pub const SECURITY_TYPE_INDEX: i32 = 6;
9pub const SECURITY_TYPE_FUTURE: i32 = 10;
10pub const SECURITY_TYPE_CRYPTO: i32 = 12;
11pub const QOT_MARKET_CC_SECURITY: i32 = 91;
12pub const QOT_RIGHT_UNKNOWN: i32 = 0;
13pub const QOT_RIGHT_BMP: i32 = 1;
14pub const QOT_RIGHT_LEVEL1: i32 = 2;
15pub const QOT_RIGHT_NO: i32 = 5;
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct SecurityRightClass {
19    pub sec_type: i32,
20    pub mkt_id: u32,
21    pub option_code_like: bool,
22}
23
24impl SecurityRightClass {
25    fn is_option(self) -> bool {
26        self.sec_type == SECURITY_TYPE_DRVT || self.option_code_like
27    }
28
29    fn is_future(self, public_market: i32) -> bool {
30        self.sec_type == SECURITY_TYPE_FUTURE
31            || (public_market == 11 && (60..=109).contains(&self.mkt_id))
32    }
33
34    fn is_index(self) -> bool {
35        self.sec_type == SECURITY_TYPE_INDEX
36    }
37
38    fn is_crypto(self, public_market: i32) -> bool {
39        self.sec_type == SECURITY_TYPE_CRYPTO
40            || public_market == QOT_MARKET_CC_SECURITY
41            || (360..=459).contains(&self.mkt_id)
42    }
43}
44
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
46pub struct QotRightSnapshot {
47    pub hk_qot_right: i32,
48    pub us_qot_right: i32,
49    pub sh_qot_right: i32,
50    pub sz_qot_right: i32,
51    pub hk_option_qot_right: i32,
52    pub hk_future_qot_right: i32,
53    pub hk_option_orderbook_depth: Option<u32>,
54    pub hk_future_orderbook_depth: Option<u32>,
55    pub us_option_qot_right: i32,
56    pub us_index_qot_right: i32,
57    pub us_otc_qot_right: i32,
58    pub us_cme_future_qot_right: i32,
59    pub us_cbot_future_qot_right: i32,
60    pub us_nymex_future_qot_right: i32,
61    pub us_comex_future_qot_right: i32,
62    pub us_cboe_future_qot_right: i32,
63    pub sg_future_qot_right: i32,
64    pub jp_future_qot_right: i32,
65    pub cc_qot_right: i32,
66}
67
68fn qot_right_denies_realtime(right: i32) -> bool {
69    matches!(right, QOT_RIGHT_BMP | QOT_RIGHT_NO)
70}
71
72fn us_future_right_for_mkt(rights: &QotRightSnapshot, mkt_id: u32) -> i32 {
73    match mkt_id {
74        60..=69 => rights.us_nymex_future_qot_right,
75        70..=79 => rights.us_comex_future_qot_right,
76        80..=89 => rights.us_cbot_future_qot_right,
77        90..=99 => rights.us_cme_future_qot_right,
78        100..=109 => rights.us_cboe_future_qot_right,
79        _ => QOT_RIGHT_UNKNOWN,
80    }
81}
82
83pub fn qot_sub_right_reject_reason(
84    market: i32,
85    code: &str,
86    security: SecurityRightClass,
87    sub_types: &[i32],
88    rights: &QotRightSnapshot,
89) -> Option<String> {
90    let wants_orderbook = sub_types.contains(&2);
91    let wants_ticker = sub_types.contains(&4);
92    let wants_broker = sub_types.contains(&14);
93    let is_option = security.is_option();
94    let is_future = security.is_future(market);
95    let is_index = security.is_index();
96    let is_crypto = security.is_crypto(market);
97    let is_other = !is_option && !is_future && !is_index;
98
99    match market {
100        _ if is_crypto && qot_right_denies_realtime(rights.cc_qot_right) => {
101            Some(format!("Subscribe: crypto 行情权限不足,不能订阅 {code}。"))
102        }
103        21 if qot_right_denies_realtime(rights.sh_qot_right) => {
104            Some("Subscribe: 沪股行情权限不足,不能订阅 SH market。".to_string())
105        }
106        22 if qot_right_denies_realtime(rights.sz_qot_right) => {
107            Some("Subscribe: 深股行情权限不足,不能订阅 SZ market。".to_string())
108        }
109        31 if qot_right_denies_realtime(rights.sg_future_qot_right) => {
110            Some("Subscribe: 新加坡行情权限不足,不能订阅 SG market。".to_string())
111        }
112        41 if qot_right_denies_realtime(rights.jp_future_qot_right) => {
113            Some("Subscribe: 日本行情权限不足,不能订阅 JP market。".to_string())
114        }
115        1 if is_other || is_index => {
116            if qot_right_denies_realtime(rights.hk_qot_right) {
117                Some(format!("Subscribe: HK 行情权限不足,不能订阅 {code}。"))
118            } else if wants_broker && rights.hk_qot_right == QOT_RIGHT_LEVEL1 {
119                Some(format!(
120                    "Subscribe: HK Level1 权限不支持 broker queue 订阅 ({code})。"
121                ))
122            } else {
123                None
124            }
125        }
126        1 if is_option => {
127            if qot_right_denies_realtime(rights.hk_option_qot_right) {
128                Some(format!(
129                    "Subscribe: HK option 行情权限不足,不能订阅 {code}。"
130                ))
131            } else if wants_orderbook && rights.hk_option_orderbook_depth == Some(0) {
132                Some(format!(
133                    "Subscribe: HK option order book depth 为 0,不能订阅摆盘 ({code})。"
134                ))
135            } else if wants_ticker && rights.hk_option_qot_right == QOT_RIGHT_LEVEL1 {
136                Some(format!(
137                    "Subscribe: HK option Level1 权限不支持 ticker 订阅 ({code});Ticker 需要 LV2。"
138                ))
139            } else {
140                None
141            }
142        }
143        1 if is_future => {
144            if qot_right_denies_realtime(rights.hk_future_qot_right) {
145                Some(format!(
146                    "Subscribe: HK future 行情权限不足,不能订阅 {code}。"
147                ))
148            } else if wants_orderbook && rights.hk_future_orderbook_depth == Some(0) {
149                Some(format!(
150                    "Subscribe: HK future order book depth 为 0,不能订阅摆盘 ({code})。"
151                ))
152            } else if wants_ticker && rights.hk_future_qot_right == QOT_RIGHT_LEVEL1 {
153                Some(format!(
154                    "Subscribe: HK future Level1 权限不支持 ticker 订阅 ({code});Ticker 需要 LV2。"
155                ))
156            } else {
157                None
158            }
159        }
160        11 if is_option && qot_right_denies_realtime(rights.us_option_qot_right) => Some(format!(
161            "Subscribe: US option 行情权限不足,不能订阅 {code}。"
162        )),
163        11 if is_future => {
164            let right = us_future_right_for_mkt(rights, security.mkt_id);
165            if right == QOT_RIGHT_UNKNOWN || qot_right_denies_realtime(right) {
166                Some(format!(
167                    "Subscribe: US future 行情权限不足或未知,不能订阅 {code}。"
168                ))
169            } else {
170                None
171            }
172        }
173        11 if is_index && qot_right_denies_realtime(rights.us_index_qot_right) => Some(format!(
174            "Subscribe: US index 行情权限不足,不能订阅 {code}。"
175        )),
176        11 if security.mkt_id == 13 && qot_right_denies_realtime(rights.us_otc_qot_right) => {
177            Some(format!("Subscribe: US OTC 行情权限不足,不能订阅 {code}。"))
178        }
179        11 if is_other && qot_right_denies_realtime(rights.us_qot_right) => {
180            Some(format!("Subscribe: US 行情权限不足,不能订阅 {code}。"))
181        }
182        _ => None,
183    }
184}
185
186#[cfg(test)]
187mod tests;