1use futu_core::diagnostic_text::{
8 jp_stock_quote_permission_hint, quote_permission_action_for_market,
9};
10
11pub const SECURITY_TYPE_DRVT: i32 = 8;
12pub const SECURITY_TYPE_INDEX: i32 = 6;
13pub const SECURITY_TYPE_FUTURE: i32 = 10;
14pub const SECURITY_TYPE_CRYPTO: i32 = 12;
15pub const QOT_MARKET_CC_SECURITY: i32 = 91;
16pub const QOT_RIGHT_UNKNOWN: i32 = 0;
17pub const QOT_RIGHT_BMP: i32 = 1;
18pub const QOT_RIGHT_LEVEL1: i32 = 2;
19pub const QOT_RIGHT_NO: i32 = 5;
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct SecurityRightClass {
23 pub sec_type: i32,
24 pub mkt_id: u32,
25 pub option_code_like: bool,
26}
27
28impl SecurityRightClass {
29 fn is_option(self) -> bool {
30 self.sec_type == SECURITY_TYPE_DRVT || self.option_code_like
31 }
32
33 fn is_future(self, public_market: i32) -> bool {
34 self.sec_type == SECURITY_TYPE_FUTURE
35 || (public_market == 11 && (60..=109).contains(&self.mkt_id))
36 }
37
38 fn is_hk_future(self) -> bool {
39 self.sec_type == SECURITY_TYPE_FUTURE && matches!(self.mkt_id, 5 | 6 | 110..=119)
43 }
44
45 fn is_us_security(self, public_market: i32) -> bool {
46 matches!(self.mkt_id, 10..=29 | 1200..=1249)
47 || (public_market == 11 && !self.is_future(public_market) && !self.is_option())
48 }
49
50 fn is_us_otc(self) -> bool {
51 self.mkt_id == 13
54 }
55
56 fn is_index(self) -> bool {
57 self.sec_type == SECURITY_TYPE_INDEX
58 }
59
60 fn is_crypto(self, public_market: i32) -> bool {
61 self.sec_type == SECURITY_TYPE_CRYPTO
62 || public_market == QOT_MARKET_CC_SECURITY
63 || (360..=459).contains(&self.mkt_id)
64 }
65
66 fn is_sh_market(self, public_market: i32) -> bool {
67 matches!(self.mkt_id, 30 | 32 | 33 | 34 | 36..=40) || public_market == 21
68 }
69
70 fn is_sz_market(self, public_market: i32) -> bool {
71 matches!(self.mkt_id, 31 | 35) || public_market == 22
72 }
73
74 fn is_sg_future(self) -> bool {
75 self.sec_type == SECURITY_TYPE_FUTURE && (160..=179).contains(&self.mkt_id)
76 }
77
78 fn is_jp_future(self) -> bool {
79 self.sec_type == SECURITY_TYPE_FUTURE && (185..=194).contains(&self.mkt_id)
80 }
81
82 fn is_sg_security_market(self, public_market: i32) -> bool {
83 (180..=184).contains(&self.mkt_id)
86 || (public_market == 31
87 && self.sec_type != SECURITY_TYPE_FUTURE
88 && self.sec_type != SECURITY_TYPE_DRVT)
89 }
90
91 fn is_my_security_market(self, public_market: i32) -> bool {
92 (1350..=1399).contains(&self.mkt_id)
95 || (public_market == 61
96 && self.sec_type != SECURITY_TYPE_FUTURE
97 && self.sec_type != SECURITY_TYPE_DRVT)
98 }
99
100 fn is_jp_security_market(self, public_market: i32) -> bool {
101 (830..=849).contains(&self.mkt_id)
105 || (public_market == 41
106 && self.sec_type != SECURITY_TYPE_FUTURE
107 && self.sec_type != SECURITY_TYPE_DRVT
108 && !self.is_jp_future()
109 && !(800..=829).contains(&self.mkt_id))
110 }
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub struct QotRightSnapshot {
115 pub hk_qot_right: i32,
116 pub us_qot_right: i32,
117 pub sh_qot_right: i32,
118 pub sz_qot_right: i32,
119 pub hk_option_qot_right: i32,
120 pub hk_future_qot_right: i32,
121 pub hk_option_orderbook_depth: Option<u32>,
122 pub hk_future_orderbook_depth: Option<u32>,
123 pub us_option_qot_right: i32,
124 pub us_index_qot_right: i32,
125 pub us_otc_qot_right: i32,
126 pub us_cme_future_qot_right: i32,
127 pub us_cbot_future_qot_right: i32,
128 pub us_nymex_future_qot_right: i32,
129 pub us_comex_future_qot_right: i32,
130 pub us_cboe_future_qot_right: i32,
131 pub sg_future_qot_right: i32,
132 pub jp_future_qot_right: i32,
133 pub sg_stock_qot_right: i32,
134 pub my_stock_qot_right: i32,
135 pub jp_stock_qot_right: i32,
136 pub cc_qot_right: i32,
137}
138
139fn qot_right_denies_realtime(right: i32) -> bool {
140 matches!(right, QOT_RIGHT_BMP | QOT_RIGHT_NO)
141}
142
143fn qot_right_is_none(right: i32) -> bool {
144 right == QOT_RIGHT_NO
145}
146
147fn us_future_right_for_mkt(rights: &QotRightSnapshot, mkt_id: u32) -> i32 {
148 match mkt_id {
149 60..=69 => rights.us_nymex_future_qot_right,
150 70..=79 => rights.us_comex_future_qot_right,
151 80..=89 => rights.us_cbot_future_qot_right,
152 90..=99 => rights.us_cme_future_qot_right,
153 100..=109 => rights.us_cboe_future_qot_right,
154 _ => QOT_RIGHT_UNKNOWN,
155 }
156}
157
158fn qot_permission_action(market_suffix: Option<&str>, code: &str) -> String {
159 let symbol = match market_suffix {
160 Some(_) if code.contains('.') => code.to_string(),
161 Some(suffix) => format!("{suffix}.{code}"),
162 None => code.to_string(),
163 };
164 quote_permission_action_for_market(market_suffix, &symbol).text_with_code()
165}
166
167pub fn qot_read_right_reject_reason(
168 market: i32,
169 code: &str,
170 security: SecurityRightClass,
171 rights: &QotRightSnapshot,
172) -> Option<String> {
173 if security.is_us_security(market) {
178 if security.is_index() && rights.us_index_qot_right == QOT_RIGHT_BMP {
179 return Some(format!(
180 "US index 行情权限不足,不能获取 {code};{}",
181 qot_permission_action(Some("US"), code)
182 ));
183 }
184 if security.is_us_otc() && rights.us_otc_qot_right == QOT_RIGHT_BMP {
185 return Some(format!(
186 "US OTC 行情权限不足,不能获取 {code};{}",
187 qot_permission_action(Some("US"), code)
188 ));
189 }
190 if rights.us_qot_right == QOT_RIGHT_BMP {
191 return Some(format!(
192 "US 行情权限不足,不能获取 {code};{}",
193 qot_permission_action(Some("US"), code)
194 ));
195 }
196 } else if market == 11 && security.is_option() {
197 if rights.us_option_qot_right == QOT_RIGHT_BMP {
198 return Some(format!(
199 "US option 行情权限不足,不能获取 {code};{}",
200 qot_permission_action(Some("US"), code)
201 ));
202 }
203 } else if security.is_future(market) && (60..=109).contains(&security.mkt_id) {
204 if us_future_right_for_mkt(rights, security.mkt_id) == QOT_RIGHT_BMP {
205 return Some(format!(
206 "US future 行情权限不足,不能获取 {code};{}",
207 qot_permission_action(Some("US"), code)
208 ));
209 }
210 } else if security.is_sh_market(market) {
211 if rights.sh_qot_right == QOT_RIGHT_BMP {
212 return Some(format!(
213 "SH 行情权限不足,不能获取 {code};{}",
214 qot_permission_action(Some("SH"), code)
215 ));
216 }
217 } else if security.is_sz_market(market) {
218 if rights.sz_qot_right == QOT_RIGHT_BMP {
219 return Some(format!(
220 "SZ 行情权限不足,不能获取 {code};{}",
221 qot_permission_action(Some("SZ"), code)
222 ));
223 }
224 } else if security.is_sg_future() {
225 if rights.sg_future_qot_right == QOT_RIGHT_BMP {
226 return Some(format!(
227 "SG future 行情权限不足,不能获取 {code};{}",
228 qot_permission_action(Some("SG"), code)
229 ));
230 }
231 } else if security.is_jp_future() {
232 if rights.jp_future_qot_right == QOT_RIGHT_BMP {
233 return Some(format!(
234 "JP future 行情权限不足,不能获取 {code};{}",
235 qot_permission_action(Some("JP"), code)
236 ));
237 }
238 } else if security.is_sg_security_market(market) {
239 if qot_right_is_none(rights.sg_stock_qot_right) {
240 return Some(format!(
241 "SG stock 行情权限不足,不能获取 {code};{}",
242 qot_permission_action(Some("SG"), code)
243 ));
244 }
245 } else if security.is_my_security_market(market) {
246 if qot_right_is_none(rights.my_stock_qot_right) {
247 return Some(format!(
248 "MY stock 行情权限不足,不能获取 {code};{}",
249 qot_permission_action(Some("MY"), code)
250 ));
251 }
252 } else if security.is_jp_security_market(market) {
253 if qot_right_is_none(rights.jp_stock_qot_right) {
254 return Some(format!(
255 "JP stock / 日股行情权限不足,不能获取 {code};{}",
256 jp_stock_quote_permission_hint().text_with_code()
257 ));
258 }
259 } else if security.is_crypto(market) && rights.cc_qot_right != QOT_RIGHT_LEVEL1 {
260 return Some(format!(
261 "Crypto 行情权限不足,不能获取 {code};\
262 请检查 Crypto MarketCrypto quote permissions;{}。",
263 qot_permission_action(Some("Crypto"), code)
264 ));
265 }
266
267 None
268}
269
270pub fn qot_sub_right_reject_reason(
271 market: i32,
272 code: &str,
273 security: SecurityRightClass,
274 sub_types: &[i32],
275 rights: &QotRightSnapshot,
276) -> Option<String> {
277 let wants_orderbook = sub_types.contains(&2);
278 let wants_ticker = sub_types.contains(&4);
279 let wants_broker = sub_types.contains(&14);
280 let is_option = security.is_option();
281 let is_hk_future = security.is_hk_future();
282 let is_future = security.is_future(market);
283 let is_index = security.is_index();
284 let is_crypto = security.is_crypto(market);
285 let is_sg_stock = security.is_sg_security_market(market);
286 let is_my_stock = security.is_my_security_market(market);
287 let is_jp_stock = security.is_jp_security_market(market);
288 let is_other = !is_option && !is_future && !is_index;
289
290 match market {
291 _ if is_crypto && qot_right_denies_realtime(rights.cc_qot_right) => Some(format!(
292 "Subscribe: crypto 行情权限不足,不能订阅 {code};\
293 请检查 Crypto MarketCrypto quote permissions;{}。",
294 qot_permission_action(Some("Crypto"), code)
295 )),
296 21 if qot_right_denies_realtime(rights.sh_qot_right) => Some(format!(
297 "Subscribe: 沪股行情权限不足,不能订阅 SH market;{}。",
298 qot_permission_action(Some("SH"), code)
299 )),
300 22 if qot_right_denies_realtime(rights.sz_qot_right) => Some(format!(
301 "Subscribe: 深股行情权限不足,不能订阅 SZ market;{}。",
302 qot_permission_action(Some("SZ"), code)
303 )),
304 31 if security.is_sg_future() && qot_right_denies_realtime(rights.sg_future_qot_right) => {
305 Some(format!(
306 "Subscribe: SG future 行情权限不足,不能订阅 SG future;{}。",
307 qot_permission_action(Some("SG"), code)
308 ))
309 }
310 31 if is_sg_stock && qot_right_is_none(rights.sg_stock_qot_right) => Some(format!(
311 "Subscribe: SG stock 行情权限不足,不能订阅 {code};{}。",
312 qot_permission_action(Some("SG"), code)
313 )),
314 61 if is_my_stock && qot_right_is_none(rights.my_stock_qot_right) => Some(format!(
315 "Subscribe: MY stock 行情权限不足,不能订阅 {code};{}。",
316 qot_permission_action(Some("MY"), code)
317 )),
318 41 if security.is_jp_future() && qot_right_denies_realtime(rights.jp_future_qot_right) => {
319 Some(format!(
320 "Subscribe: JP future 行情权限不足,不能订阅 JP future;{}。",
321 qot_permission_action(Some("JP"), code)
322 ))
323 }
324 41 if is_jp_stock && qot_right_is_none(rights.jp_stock_qot_right) => Some(format!(
325 "Subscribe: JP stock / 日股行情权限不足,不能订阅 {code};{}",
326 jp_stock_quote_permission_hint().text_with_code()
327 )),
328 1 if is_other || is_index => {
329 if qot_right_denies_realtime(rights.hk_qot_right) {
330 Some(format!(
331 "Subscribe: HK 行情权限不足,不能订阅 {code};{}。",
332 qot_permission_action(Some("HK"), code)
333 ))
334 } else if wants_broker && rights.hk_qot_right == QOT_RIGHT_LEVEL1 {
335 Some(format!(
336 "Subscribe: HK Level1 权限不支持 broker queue 订阅 ({code})。"
337 ))
338 } else {
339 None
340 }
341 }
342 1 if is_option => {
343 if qot_right_denies_realtime(rights.hk_option_qot_right) {
344 Some(format!(
345 "Subscribe: HK option 行情权限不足,不能订阅 {code}。"
346 ))
347 } else if wants_orderbook && rights.hk_option_orderbook_depth == Some(0) {
348 Some(format!(
349 "Subscribe: HK option order book depth 为 0,不能订阅摆盘 ({code})。"
350 ))
351 } else if wants_ticker && rights.hk_option_qot_right == QOT_RIGHT_LEVEL1 {
352 Some(format!(
353 "Subscribe: HK option Level1 权限不支持 ticker 订阅 ({code});Ticker 需要 LV2。"
354 ))
355 } else {
356 None
357 }
358 }
359 1 if is_hk_future => {
360 if qot_right_denies_realtime(rights.hk_future_qot_right) {
361 Some(format!(
362 "Subscribe: HK future 行情权限不足,不能订阅 {code}。"
363 ))
364 } else if wants_orderbook && rights.hk_future_orderbook_depth == Some(0) {
365 Some(format!(
366 "Subscribe: HK future order book depth 为 0,不能订阅摆盘 ({code})。"
367 ))
368 } else if wants_ticker && rights.hk_future_qot_right == QOT_RIGHT_LEVEL1 {
369 Some(format!(
370 "Subscribe: HK future Level1 权限不支持 ticker 订阅 ({code});Ticker 需要 LV2。"
371 ))
372 } else {
373 None
374 }
375 }
376 11 if is_option && qot_right_denies_realtime(rights.us_option_qot_right) => Some(format!(
377 "Subscribe: US option 行情权限不足,不能订阅 {code};{}。",
378 qot_permission_action(Some("US"), code)
379 )),
380 11 if is_future => {
381 let right = us_future_right_for_mkt(rights, security.mkt_id);
382 if right == QOT_RIGHT_UNKNOWN || qot_right_denies_realtime(right) {
383 Some(format!(
384 "Subscribe: US future 行情权限不足或未知,不能订阅 {code};{}。",
385 qot_permission_action(Some("US"), code)
386 ))
387 } else {
388 None
389 }
390 }
391 11 if is_index && qot_right_denies_realtime(rights.us_index_qot_right) => Some(format!(
392 "Subscribe: US index 行情权限不足,不能订阅 {code};{}。",
393 qot_permission_action(Some("US"), code)
394 )),
395 11 if security.mkt_id == 13 && qot_right_denies_realtime(rights.us_otc_qot_right) => {
396 Some(format!(
397 "Subscribe: US OTC 行情权限不足,不能订阅 {code};{}。",
398 qot_permission_action(Some("US"), code)
399 ))
400 }
401 11 if is_other && qot_right_denies_realtime(rights.us_qot_right) => Some(format!(
402 "Subscribe: US 行情权限不足,不能订阅 {code};{}。",
403 qot_permission_action(Some("US"), code)
404 )),
405 _ => None,
406 }
407}
408
409#[cfg(test)]
410mod tests;