Skip to main content

futu_core/qot_right_gate/
read.rs

1use crate::diagnostic_text::jp_stock_quote_permission_hint;
2use crate::diagnostic_text::{DiagnosticTextKey, DiagnosticTextSpec, MessageArgs};
3
4use super::common::{
5    qot_permission_action, qot_right_denies_realtime, qot_right_is_none, us_future_right_for_mkt,
6};
7use super::constants::{QOT_RIGHT_BMP, QOT_RIGHT_LEVEL1, QOT_RIGHT_NO};
8use super::model::{QotRightSnapshot, SecurityRightClass};
9
10const QOT_MARKET_EVENT_CONTRACT: i32 = 101;
11const QOT_MARKET_JP_SECURITY: i32 = 41;
12const SECURITY_TYPE_EQTY: i32 = 3;
13const REPRESENTATIVE_JP_CASH_TSE_MKT_ID: u32 = 830;
14
15/// C++ `IsHasRightToSub` treats a US index under internal BMP as an unsupported
16/// product, not as an actionable permission/subscription failure. The runtime
17/// cache stores the API-normalized value, where US internal BMP maps to public
18/// `QotRight_No=5`, not the HK/CN public `QotRight_Bmp=1`. Qot_Sub and GetKL
19/// share this typed decision so their public errors cannot diverge.
20///
21/// Ref: `NNBiz_Qot_Right.cpp:632-664`,
22/// `APIServer_Inner_API.cpp:3038-3045,5658-5668`, and
23/// `APIServer_Qot_KL.cpp:46-117`.
24pub fn us_index_no_right_unsupported_diagnostic(
25    endpoint: &'static str,
26    market: i32,
27    code: &str,
28    security: SecurityRightClass,
29    rights: &QotRightSnapshot,
30) -> Option<DiagnosticTextSpec> {
31    (market == 11 && security.is_index() && rights.us_index_qot_right == QOT_RIGHT_NO).then(|| {
32        DiagnosticTextSpec::new(
33            DiagnosticTextKey::QotUsIndexUnsupported,
34            MessageArgs::new()
35                .with("endpoint", endpoint)
36                .with("code", code),
37        )
38    })
39}
40
41pub fn qot_read_right_reject_reason(
42    market: i32,
43    code: &str,
44    security: SecurityRightClass,
45    rights: &QotRightSnapshot,
46) -> Option<String> {
47    // C++ CheckHasQotRight gates read-style QOT/F10 endpoints and is deliberately
48    // narrower than IsHasRightToSub. Notably, ordinary HK securities are not
49    // rejected here merely because HK real-time rights are BMP.
50    // Ref: FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:1037-1143.
51    // Public market 101 is the fixed FTAPI EventContract ABI value. C++
52    // identifies EventContract from the public market and rejects the
53    // explicit None right before serving quote data.
54    // Ref: APIServer_Inner_API.cpp:635-638,1288-1295.
55    if market == QOT_MARKET_EVENT_CONTRACT && rights.event_contract_qot_right == QOT_RIGHT_NO {
56        return Some(format!(
57            "EventContract 行情权限不足,不能获取 {code};\
58             请检查 EventContract quote permissions。"
59        ));
60    }
61
62    if security.is_us_security(market) {
63        if security.is_index() && qot_right_denies_realtime(rights.us_index_qot_right) {
64            return Some(format!(
65                "US index 行情权限不足,不能获取 {code};{}",
66                qot_permission_action(Some("US"), code)
67            ));
68        }
69        if security.is_us_otc() && qot_right_denies_realtime(rights.us_otc_qot_right) {
70            return Some(format!(
71                "US OTC 行情权限不足,不能获取 {code};{}",
72                qot_permission_action(Some("US"), code)
73            ));
74        }
75        if qot_right_denies_realtime(rights.us_qot_right) {
76            return Some(format!(
77                "US 行情权限不足,不能获取 {code};{}",
78                qot_permission_action(Some("US"), code)
79            ));
80        }
81    } else if market == 11 && security.is_option() {
82        if rights.us_option_qot_right == QOT_RIGHT_BMP {
83            return Some(format!(
84                "US option 行情权限不足,不能获取 {code};{}",
85                qot_permission_action(Some("US"), code)
86            ));
87        }
88    } else if security.is_future(market) && (60..=109).contains(&security.mkt_id) {
89        if us_future_right_for_mkt(rights, security.mkt_id) == QOT_RIGHT_BMP {
90            return Some(format!(
91                "US future 行情权限不足,不能获取 {code};{}",
92                qot_permission_action(Some("US"), code)
93            ));
94        }
95    } else if security.is_sh_market(market) {
96        if rights.sh_qot_right == QOT_RIGHT_BMP {
97            return Some(format!(
98                "SH 行情权限不足,不能获取 {code};{}",
99                qot_permission_action(Some("SH"), code)
100            ));
101        }
102    } else if security.is_sz_market(market) {
103        if rights.sz_qot_right == QOT_RIGHT_BMP {
104            return Some(format!(
105                "SZ 行情权限不足,不能获取 {code};{}",
106                qot_permission_action(Some("SZ"), code)
107            ));
108        }
109    } else if security.is_sg_future() {
110        if rights.sg_future_qot_right == QOT_RIGHT_BMP {
111            return Some(format!(
112                "SG future 行情权限不足,不能获取 {code};{}",
113                qot_permission_action(Some("SG"), code)
114            ));
115        }
116    } else if security.is_jp_future() {
117        if rights.jp_future_qot_right == QOT_RIGHT_BMP {
118            return Some(format!(
119                "JP future 行情权限不足,不能获取 {code};{}",
120                qot_permission_action(Some("JP"), code)
121            ));
122        }
123    } else if security.is_sg_security_market(market) {
124        if qot_right_is_none(rights.sg_stock_qot_right) {
125            return Some(format!(
126                "SG stock 行情权限不足,不能获取 {code};{}",
127                qot_permission_action(Some("SG"), code)
128            ));
129        }
130    } else if security.is_my_security_market(market) {
131        if qot_right_is_none(rights.my_stock_qot_right) {
132            return Some(format!(
133                "MY stock 行情权限不足,不能获取 {code};{}",
134                qot_permission_action(Some("MY"), code)
135            ));
136        }
137    } else if security.is_jp_security_market(market) {
138        if qot_right_is_none(rights.jp_stock_qot_right) {
139            return Some(format!(
140                "JP stock / 日股行情权限不足,不能获取 {code};{}",
141                jp_stock_quote_permission_hint().text_with_code()
142            ));
143        }
144    } else if security.is_crypto(market) && rights.cc_qot_right != QOT_RIGHT_LEVEL1 {
145        return Some(format!(
146            "Crypto 行情权限不足,不能获取 {code};\
147             请检查 Crypto MarketCrypto quote permissions;{}。",
148            qot_permission_action(Some("Crypto"), code)
149        ));
150    }
151
152    None
153}
154
155pub fn qot_read_right_reject_reason_for_jp_stock_without_static(
156    market: i32,
157    code: &str,
158    rights: &QotRightSnapshot,
159) -> Option<String> {
160    if market != QOT_MARKET_JP_SECURITY {
161        return None;
162    }
163
164    // Static cache miss means we do not yet know the exact JP sub-market, but
165    // public QotMarket=41 + an explicit "no JP stock right" is enough to give
166    // the actionable C++-aligned right-gate hint instead of masking it as
167    // "unknown security". Use a representative JP cash mkt_id in the TSE
168    // range; C++ APIServer_Inner_API.cpp:1245-1252 / 4505-4512 gates JP cash
169    // securities on JP stock quote right.
170    qot_read_right_reject_reason(
171        market,
172        code,
173        SecurityRightClass {
174            sec_type: SECURITY_TYPE_EQTY,
175            mkt_id: REPRESENTATIVE_JP_CASH_TSE_MKT_ID,
176            option_code_like: false,
177        },
178        rights,
179    )
180}