1use futu_core::qot_stock_key::QotSecurityKey;
8
9use crate::types::{KLType, SubType};
10
11pub const SESSION_NONE: i32 = 0;
12pub const SESSION_RTH: i32 = 1;
13pub const SESSION_ETH: i32 = 2;
14pub const SESSION_ALL: i32 = 3;
15pub const SESSION_OVERNIGHT: i32 = 4;
16pub const SECURITY_TYPE_DRVT: i32 = 8;
17pub const SECURITY_TYPE_FUTURE: i32 = 10;
18
19pub const BACKEND_MARKET_HK_OPTION: i32 = 9;
25pub const BACKEND_MARKET_US_OPTION: i32 = 15;
26
27pub const SUB_TYPE_NONE: i32 = SubType::None as i32;
31pub const SUB_TYPE_BASIC: i32 = SubType::Basic as i32;
32pub const SUB_TYPE_ORDER_BOOK: i32 = SubType::OrderBook as i32;
33pub const SUB_TYPE_TICKER: i32 = SubType::Ticker as i32;
34pub const SUB_TYPE_RT: i32 = SubType::RT as i32;
35pub const SUB_TYPE_KL_DAY: i32 = SubType::KLDay as i32;
36pub const SUB_TYPE_KL_5MIN: i32 = SubType::KL5Min as i32;
37pub const SUB_TYPE_KL_15MIN: i32 = SubType::KL15Min as i32;
38pub const SUB_TYPE_KL_30MIN: i32 = SubType::KL30Min as i32;
39pub const SUB_TYPE_KL_60MIN: i32 = SubType::KL60Min as i32;
40pub const SUB_TYPE_KL_1MIN: i32 = SubType::KL1Min as i32;
41pub const SUB_TYPE_KL_WEEK: i32 = SubType::KLWeek as i32;
42pub const SUB_TYPE_KL_MONTH: i32 = SubType::KLMonth as i32;
43pub const SUB_TYPE_BROKER: i32 = SubType::Broker as i32;
44pub const SUB_TYPE_KL_QUARTER: i32 = SubType::KLQuarter as i32;
45pub const SUB_TYPE_KL_YEAR: i32 = SubType::KLYear as i32;
46pub const SUB_TYPE_KL_3MIN: i32 = SubType::KL3Min as i32;
47pub const SUB_TYPE_KL_10MIN: i32 = SubType::KL10Min as i32;
48pub const SUB_TYPE_KL_120MIN: i32 = SubType::KL120Min as i32;
49pub const SUB_TYPE_KL_180MIN: i32 = SubType::KL180Min as i32;
50pub const SUB_TYPE_KL_240MIN: i32 = SubType::KL240Min as i32;
51pub const SUB_TYPE_ORDER_BOOK_ODD: i32 = SubType::OrderBookOdd as i32;
52
53pub const VALID_QOT_SUB_TYPES: &[i32] = SubType::VALID_PUBLIC_INT_VALUES;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct SubscribeOptionsPlan {
57 pub requested_session: i32,
58 pub backend_session: i32,
59 pub extended_time: bool,
60 pub orderbook_detail: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct RegQotPushResolvedSecurity {
65 pub sec_key: String,
66 pub stock_id: u64,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum RegQotPushLookup {
71 Missing,
72 Present { stock_id: u64 },
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum SubscribePlanError {
77 OvernightSessionUnsupported,
78}
79
80impl SubscribeOptionsPlan {
81 pub fn from_raw(
87 requested_session: Option<i32>,
88 extended_time: Option<bool>,
89 orderbook_detail: Option<bool>,
90 ) -> Result<Self, SubscribePlanError> {
91 let requested_session = requested_session.unwrap_or(SESSION_NONE);
92 if requested_session == SESSION_OVERNIGHT {
93 return Err(SubscribePlanError::OvernightSessionUnsupported);
94 }
95
96 let extended_time = extended_time.unwrap_or(false);
97 let backend_session = if requested_session == SESSION_NONE {
98 if extended_time {
99 SESSION_ETH
100 } else {
101 SESSION_RTH
102 }
103 } else {
104 requested_session
105 };
106
107 Ok(Self {
108 requested_session,
109 backend_session,
110 extended_time,
111 orderbook_detail: orderbook_detail.unwrap_or(false),
112 })
113 }
114}
115
116#[must_use]
123pub fn is_valid_qot_market(market: i32) -> bool {
124 matches!(market, 1 | 11 | 21 | 22 | 31 | 41 | 51 | 61 | 71 | 81 | 91)
125}
126
127#[must_use]
135pub fn normalize_qot_sub_market(market: i32) -> Option<i32> {
136 if is_valid_qot_market(market) {
137 return Some(market);
138 }
139
140 match market {
144 2 => Some(11), 3 => Some(21), 4 => Some(1), 5 => Some(1), 6 => Some(31), 7 => Some(91), 8 => Some(51), 15 => Some(41), 111 => Some(61), 112 => Some(71), _ => None,
155 }
156}
157
158pub fn resolve_reg_qot_push_securities<F>(
165 securities: &[futu_proto::qot_common::Security],
166 mut lookup_stock_id: F,
167) -> Result<Vec<RegQotPushResolvedSecurity>, String>
168where
169 F: FnMut(&str) -> RegQotPushLookup,
170{
171 let mut resolved = Vec::with_capacity(securities.len());
172 for sec in securities {
173 if sec.code.trim().is_empty() {
174 return Err(
175 "RegQotPush: code 不能为空。C++ 会先对 securityList 逐项调用 GetStockID,\
176 空 code 不能进入 push 注册/取消。"
177 .to_string(),
178 );
179 }
180 if !is_valid_qot_market(sec.market) {
181 return Err(format!(
182 "RegQotPush: 非法 market={} for code={}。valid QotMarket: \
183 1=HK/11=US/21=CNSH/22=CNSZ/31=SG/41=JP/51=AU/61=MY/71=CA/81=FX/91=CC。",
184 sec.market, sec.code
185 ));
186 }
187
188 let sec_key = format!("{}_{}", sec.market, sec.code);
189 match lookup_stock_id(&sec_key) {
190 RegQotPushLookup::Missing => {
191 return Err(format!(
192 "RegQotPush: 未知证券 {}。C++ APIServer_Qot_RegQotPush.cpp:36-47 \
193 在 register/unregister 前都会先 GetStockID;daemon 无法从静态表解析该证券,\
194 不执行 push 注册状态变更。",
195 sec_key
196 ));
197 }
198 RegQotPushLookup::Present { stock_id: 0 } => {
199 return Err(format!(
200 "RegQotPush: 证券 {} 缺少 stock_id。C++ 需要 GetStockID 成功后才会调用 \
201 RegOrUnRegPush;daemon 不执行 push 注册状态变更。",
202 sec_key
203 ));
204 }
205 RegQotPushLookup::Present { stock_id } => {
206 resolved.push(RegQotPushResolvedSecurity { sec_key, stock_id });
207 }
208 }
209 }
210 Ok(resolved)
211}
212
213#[must_use]
214pub fn is_kl_sub_type(sub_type: i32) -> bool {
215 SubType::from_i32(sub_type)
216 .map(SubType::is_kline)
217 .unwrap_or(false)
218}
219
220#[must_use]
221pub fn is_valid_sub_type(sub_type: i32) -> bool {
222 SubType::from_public_i32(sub_type).is_some()
223}
224
225#[must_use]
230pub fn unsupported_option_sub_type_name(sub_type: i32) -> Option<&'static str> {
231 match SubType::from_i32(sub_type)? {
232 SubType::None => Some("None"),
233 SubType::KL30Min => Some("KL_30Min"),
234 SubType::KLWeek => Some("KL_Week"),
235 SubType::KLMonth => Some("KL_Month"),
236 SubType::KLQuarter => Some("KL_Quarter"),
237 SubType::KLYear => Some("KL_Year"),
238 SubType::KL3Min => Some("KL_3Min"),
239 _ => None,
240 }
241}
242
243#[must_use]
244pub fn reg_push_rehab_types(sub_type: i32, requested: &[i32]) -> Vec<i32> {
245 if is_kl_sub_type(sub_type) {
246 if requested.is_empty() {
247 vec![1]
249 } else {
250 requested.to_vec()
251 }
252 } else {
253 vec![0]
254 }
255}
256
257#[must_use]
258pub fn kl_type_for_sub_type(sub_type: i32) -> Option<i32> {
259 SubType::from_i32(sub_type)
260 .and_then(SubType::kl_type)
261 .map(KLType::as_i32)
262}
263
264#[must_use]
265pub fn backend_subscribe_market_for_security(
266 requested_market: i32,
267 sec_type: i32,
268 mkt_id: u32,
269) -> i32 {
270 if sec_type == SECURITY_TYPE_DRVT {
271 return match mkt_id {
272 7 | 8 | 570..=579 => BACKEND_MARKET_HK_OPTION,
277 41..=45 => BACKEND_MARKET_US_OPTION,
278 _ => match requested_market {
282 1 => BACKEND_MARKET_HK_OPTION,
283 11 => BACKEND_MARKET_US_OPTION,
284 _ => requested_market,
285 },
286 };
287 }
288
289 if sec_type != SECURITY_TYPE_FUTURE {
290 return requested_market;
291 }
292
293 match mkt_id {
294 5 => 5,
299 6 | 110..=119 => 6,
300 60..=109 => 14,
301 160..=179 => 13,
302 185..=194 => 16,
303 1400..=1449 => requested_market,
308 _ => match requested_market {
309 1 | 2 => 6,
310 11 => 14,
311 31 => 13,
312 41 => 16,
313 _ => requested_market,
314 },
315 }
316}
317
318#[must_use]
324pub fn public_market_from_sec_key(sec_key: &str) -> Option<i32> {
325 QotSecurityKey::parse_public_sec_key(sec_key).map(|key| key.market)
326}
327
328#[must_use]
336pub fn backend_desired_key_for_sec_key(
337 sec_key: &str,
338 stock_id: u64,
339 sec_type: i32,
340 mkt_id: u32,
341) -> Option<(u64, i32)> {
342 if stock_id == 0 {
343 return None;
344 }
345 let public_market = public_market_from_sec_key(sec_key)?;
346 Some((
347 stock_id,
348 backend_subscribe_market_for_security(public_market, sec_type, mkt_id),
349 ))
350}
351
352#[cfg(test)]
353mod tests;