1pub const SESSION_NONE: i32 = 0;
8pub const SESSION_RTH: i32 = 1;
9pub const SESSION_ETH: i32 = 2;
10pub const SESSION_ALL: i32 = 3;
11pub const SESSION_OVERNIGHT: i32 = 5;
12pub const SECURITY_TYPE_DRVT: i32 = 8;
13pub const SECURITY_TYPE_FUTURE: i32 = 10;
14
15pub const BACKEND_MARKET_HK_OPTION: i32 = 9;
21pub const BACKEND_MARKET_US_OPTION: i32 = 15;
22
23pub const SUB_TYPE_NONE: i32 = 0;
27pub const SUB_TYPE_BASIC: i32 = 1;
28pub const SUB_TYPE_ORDER_BOOK: i32 = 2;
29pub const SUB_TYPE_TICKER: i32 = 4;
30pub const SUB_TYPE_RT: i32 = 5;
31pub const SUB_TYPE_KL_DAY: i32 = 6;
32pub const SUB_TYPE_KL_5MIN: i32 = 7;
33pub const SUB_TYPE_KL_15MIN: i32 = 8;
34pub const SUB_TYPE_KL_30MIN: i32 = 9;
35pub const SUB_TYPE_KL_60MIN: i32 = 10;
36pub const SUB_TYPE_KL_1MIN: i32 = 11;
37pub const SUB_TYPE_KL_WEEK: i32 = 12;
38pub const SUB_TYPE_KL_MONTH: i32 = 13;
39pub const SUB_TYPE_BROKER: i32 = 14;
40pub const SUB_TYPE_KL_QUARTER: i32 = 15;
41pub const SUB_TYPE_KL_YEAR: i32 = 16;
42pub const SUB_TYPE_KL_3MIN: i32 = 17;
43
44pub const VALID_QOT_SUB_TYPES: &[i32] = &[
45 SUB_TYPE_BASIC,
46 SUB_TYPE_ORDER_BOOK,
47 SUB_TYPE_TICKER,
48 SUB_TYPE_RT,
49 SUB_TYPE_KL_DAY,
50 SUB_TYPE_KL_5MIN,
51 SUB_TYPE_KL_15MIN,
52 SUB_TYPE_KL_30MIN,
53 SUB_TYPE_KL_60MIN,
54 SUB_TYPE_KL_1MIN,
55 SUB_TYPE_KL_WEEK,
56 SUB_TYPE_KL_MONTH,
57 SUB_TYPE_BROKER,
58 SUB_TYPE_KL_QUARTER,
59 SUB_TYPE_KL_YEAR,
60 SUB_TYPE_KL_3MIN,
61];
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct SubscribeOptionsPlan {
65 pub requested_session: i32,
66 pub backend_session: i32,
67 pub extended_time: bool,
68 pub orderbook_detail: bool,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct RegQotPushResolvedSecurity {
73 pub sec_key: String,
74 pub stock_id: u64,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum RegQotPushLookup {
79 Missing,
80 Present { stock_id: u64 },
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum SubscribePlanError {
85 OvernightSessionUnsupported,
86}
87
88impl SubscribeOptionsPlan {
89 pub fn from_raw(
95 requested_session: Option<i32>,
96 extended_time: Option<bool>,
97 orderbook_detail: Option<bool>,
98 ) -> Result<Self, SubscribePlanError> {
99 let requested_session = requested_session.unwrap_or(SESSION_NONE);
100 if requested_session == SESSION_OVERNIGHT {
101 return Err(SubscribePlanError::OvernightSessionUnsupported);
102 }
103
104 let extended_time = extended_time.unwrap_or(false);
105 let backend_session = if requested_session == SESSION_NONE {
106 if extended_time {
107 SESSION_ETH
108 } else {
109 SESSION_RTH
110 }
111 } else {
112 requested_session
113 };
114
115 Ok(Self {
116 requested_session,
117 backend_session,
118 extended_time,
119 orderbook_detail: orderbook_detail.unwrap_or(false),
120 })
121 }
122}
123
124#[must_use]
131pub fn is_valid_qot_market(market: i32) -> bool {
132 matches!(market, 1 | 11 | 21 | 22 | 31 | 41 | 51 | 61 | 71 | 81 | 91)
133}
134
135#[must_use]
143pub fn normalize_qot_sub_market(market: i32) -> Option<i32> {
144 if is_valid_qot_market(market) {
145 return Some(market);
146 }
147
148 match market {
152 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,
163 }
164}
165
166pub fn resolve_reg_qot_push_securities<F>(
173 securities: &[futu_proto::qot_common::Security],
174 mut lookup_stock_id: F,
175) -> Result<Vec<RegQotPushResolvedSecurity>, String>
176where
177 F: FnMut(&str) -> RegQotPushLookup,
178{
179 let mut resolved = Vec::with_capacity(securities.len());
180 for sec in securities {
181 if sec.code.trim().is_empty() {
182 return Err(
183 "RegQotPush: code 不能为空。C++ 会先对 securityList 逐项调用 GetStockID,\
184 空 code 不能进入 push 注册/取消。"
185 .to_string(),
186 );
187 }
188 if !is_valid_qot_market(sec.market) {
189 return Err(format!(
190 "RegQotPush: 非法 market={} for code={}。valid QotMarket: \
191 1=HK/11=US/21=CNSH/22=CNSZ/31=SG/41=JP/51=AU/61=MY/71=CA/81=FX/91=CC。",
192 sec.market, sec.code
193 ));
194 }
195
196 let sec_key = format!("{}_{}", sec.market, sec.code);
197 match lookup_stock_id(&sec_key) {
198 RegQotPushLookup::Missing => {
199 return Err(format!(
200 "RegQotPush: 未知证券 {}。C++ APIServer_Qot_RegQotPush.cpp:36-47 \
201 在 register/unregister 前都会先 GetStockID;daemon 无法从静态表解析该证券,\
202 不执行 push 注册状态变更。",
203 sec_key
204 ));
205 }
206 RegQotPushLookup::Present { stock_id: 0 } => {
207 return Err(format!(
208 "RegQotPush: 证券 {} 缺少 stock_id。C++ 需要 GetStockID 成功后才会调用 \
209 RegOrUnRegPush;daemon 不执行 push 注册状态变更。",
210 sec_key
211 ));
212 }
213 RegQotPushLookup::Present { stock_id } => {
214 resolved.push(RegQotPushResolvedSecurity { sec_key, stock_id });
215 }
216 }
217 }
218 Ok(resolved)
219}
220
221#[must_use]
222pub fn is_kl_sub_type(sub_type: i32) -> bool {
223 matches!(
224 sub_type,
225 SUB_TYPE_KL_DAY
226 | SUB_TYPE_KL_5MIN
227 | SUB_TYPE_KL_15MIN
228 | SUB_TYPE_KL_30MIN
229 | SUB_TYPE_KL_60MIN
230 | SUB_TYPE_KL_1MIN
231 | SUB_TYPE_KL_WEEK
232 | SUB_TYPE_KL_MONTH
233 | SUB_TYPE_KL_QUARTER
234 | SUB_TYPE_KL_YEAR
235 | SUB_TYPE_KL_3MIN
236 )
237}
238
239#[must_use]
240pub fn is_valid_sub_type(sub_type: i32) -> bool {
241 VALID_QOT_SUB_TYPES.contains(&sub_type)
242}
243
244#[must_use]
249pub fn unsupported_option_sub_type_name(sub_type: i32) -> Option<&'static str> {
250 match sub_type {
251 SUB_TYPE_NONE => Some("None"),
252 SUB_TYPE_KL_30MIN => Some("KL_30Min"),
253 SUB_TYPE_KL_WEEK => Some("KL_Week"),
254 SUB_TYPE_KL_MONTH => Some("KL_Month"),
255 SUB_TYPE_KL_QUARTER => Some("KL_Quarter"),
256 SUB_TYPE_KL_YEAR => Some("KL_Year"),
257 SUB_TYPE_KL_3MIN => Some("KL_3Min"),
258 _ => None,
259 }
260}
261
262#[must_use]
263pub fn reg_push_rehab_types(sub_type: i32, requested: &[i32]) -> Vec<i32> {
264 if is_kl_sub_type(sub_type) {
265 if requested.is_empty() {
266 vec![1]
268 } else {
269 requested.to_vec()
270 }
271 } else {
272 vec![0]
273 }
274}
275
276#[must_use]
277pub fn kl_type_for_sub_type(sub_type: i32) -> Option<i32> {
278 match sub_type {
279 SUB_TYPE_KL_1MIN => Some(1),
280 SUB_TYPE_KL_DAY => Some(2),
281 SUB_TYPE_KL_WEEK => Some(3),
282 SUB_TYPE_KL_MONTH => Some(4),
283 SUB_TYPE_KL_YEAR => Some(5),
284 SUB_TYPE_KL_5MIN => Some(6),
285 SUB_TYPE_KL_15MIN => Some(7),
286 SUB_TYPE_KL_30MIN => Some(8),
287 SUB_TYPE_KL_60MIN => Some(9),
288 SUB_TYPE_KL_3MIN => Some(10),
289 SUB_TYPE_KL_QUARTER => Some(11),
290 _ => None,
291 }
292}
293
294#[must_use]
295pub fn backend_subscribe_market_for_security(
296 requested_market: i32,
297 sec_type: i32,
298 mkt_id: u32,
299) -> i32 {
300 if sec_type == SECURITY_TYPE_DRVT {
301 return match mkt_id {
302 7 | 8 | 570..=579 => BACKEND_MARKET_HK_OPTION,
307 41..=45 => BACKEND_MARKET_US_OPTION,
308 _ => match requested_market {
312 1 => BACKEND_MARKET_HK_OPTION,
313 11 => BACKEND_MARKET_US_OPTION,
314 _ => requested_market,
315 },
316 };
317 }
318
319 if sec_type != SECURITY_TYPE_FUTURE {
320 return requested_market;
321 }
322
323 match mkt_id {
324 5 => 5,
329 6 | 110..=119 => 6,
330 60..=109 => 14,
331 160..=179 => 13,
332 185..=194 => 16,
333 1400..=1499 => 6,
338 _ => match requested_market {
339 1 | 2 => 6,
340 11 => 14,
341 31 => 13,
342 41 => 16,
343 _ => requested_market,
344 },
345 }
346}
347
348#[must_use]
354pub fn public_market_from_sec_key(sec_key: &str) -> Option<i32> {
355 let (market, code) = sec_key.split_once('_')?;
356 if code.is_empty() {
357 return None;
358 }
359 market.parse().ok()
360}
361
362#[must_use]
370pub fn backend_desired_key_for_sec_key(
371 sec_key: &str,
372 stock_id: u64,
373 sec_type: i32,
374 mkt_id: u32,
375) -> Option<(u64, i32)> {
376 if stock_id == 0 {
377 return None;
378 }
379 let public_market = public_market_from_sec_key(sec_key)?;
380 Some((
381 stock_id,
382 backend_subscribe_market_for_security(public_market, sec_type, mkt_id),
383 ))
384}
385
386#[cfg(test)]
387mod tests;