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