1use crate::types::TrdMarket;
8use futu_core::market::TrdSecMarketId;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct RawAccountMarket(i32);
18
19impl RawAccountMarket {
20 pub const fn new(value: i32) -> Self {
21 Self(value)
22 }
23
24 pub const fn as_i32(self) -> i32 {
25 self.0
26 }
27
28 #[must_use]
29 pub fn view_only_fund_label(self) -> Option<&'static str> {
30 use crate::currency::legacy_backend_fund_market_id;
31
32 match self.0 {
33 legacy_backend_fund_market_id::HK_FUND => Some("HKFund(raw)"),
34 legacy_backend_fund_market_id::US_FUND_OLD => Some("USFund(raw,old)"),
35 legacy_backend_fund_market_id::US_FUND => Some("USFund(raw)"),
36 legacy_backend_fund_market_id::SG_FUND => Some("SGFund(raw)"),
37 _ => None,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub struct CachedAccountMarket(i32);
49
50impl CachedAccountMarket {
51 pub const fn new(value: i32) -> Self {
52 Self(value)
53 }
54
55 pub const fn as_i32(self) -> i32 {
56 self.0
57 }
58
59 #[must_use]
60 pub fn view_only_fund_label(self) -> Option<&'static str> {
61 use crate::currency::trd_market_id;
62
63 RawAccountMarket::new(self.0)
64 .view_only_fund_label()
65 .or(match self.0 {
66 trd_market_id::HK_FUND => Some("HKFund"),
67 trd_market_id::US_FUND => Some("USFund"),
68 trd_market_id::SG_FUND => Some("SGFund"),
69 trd_market_id::MY_FUND => Some("MYFund"),
70 trd_market_id::JP_FUND => Some("JPFund"),
71 _ => None,
72 })
73 }
74}
75
76pub const TRD_MARKET_STRING_VALUES: &[&str] = &[
78 "HK",
79 "US",
80 "CN",
81 "HKCC",
82 "FUTURES",
83 "SG",
84 "CRYPTO",
85 "AU",
86 "FUTURES_SIMULATE_HK",
87 "FUTURES_SIMULATE_US",
88 "FUTURES_SIMULATE_SG",
89 "FUTURES_SIMULATE_JP",
90 "JP",
91 "MY",
92 "CA",
93 "HKFUND",
94 "USFUND",
95 "SGFUND",
96 "MYFUND",
97 "JPFUND",
98];
99
100pub const TRD_MARKET_PARSE_CHOICES: &str = "HK|US|CN|HKCC|FUTURES|SG|CRYPTO|AU|\
101FUTURES_SIMULATE_HK|FUTURES_SIMULATE_US|FUTURES_SIMULATE_SG|FUTURES_SIMULATE_JP|\
102JP|MY|CA|HKFUND|USFUND|SGFUND|MYFUND|JPFUND or official TrdMarket int";
103
104pub const TRD_MARKET_NON_FUND_STRING_VALUES: &[&str] = &[
106 "HK",
107 "US",
108 "CN",
109 "HKCC",
110 "FUTURES",
111 "SG",
112 "CRYPTO",
113 "AU",
114 "FUTURES_SIMULATE_HK",
115 "FUTURES_SIMULATE_US",
116 "FUTURES_SIMULATE_SG",
117 "FUTURES_SIMULATE_JP",
118 "JP",
119 "MY",
120 "CA",
121];
122
123pub const TRD_MARKET_NON_FUND_PARSE_CHOICES: &str = "HK|US|CN|HKCC|FUTURES|SG|\
124CRYPTO|AU|FUTURES_SIMULATE_HK|FUTURES_SIMULATE_US|FUTURES_SIMULATE_SG|\
125FUTURES_SIMULATE_JP|JP|MY|CA or official non-fund TrdMarket int";
126
127pub const TRD_MARKET_INT_VALUES: &[i32] = &[
130 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 111, 112, 113, 123, 124, 125, 126,
131];
132
133pub const TRD_MARKET_NON_FUND_INT_VALUES: &[i32] =
136 &[1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 111, 112];
137
138#[must_use]
145pub fn parse_trd_market(raw: &str) -> Option<TrdMarket> {
146 let upper = raw.trim().to_ascii_uppercase();
147 let compact = upper.replace('_', "");
148 let market = match upper.as_str() {
149 "HK" => TrdMarket::HK,
150 "US" => TrdMarket::US,
151 "CN" => TrdMarket::CN,
152 "HKCC" => TrdMarket::HKCC,
153 "FUTURES" => TrdMarket::Futures,
154 "SG" => TrdMarket::SG,
155 "CRYPTO" => TrdMarket::Crypto,
156 "AU" => TrdMarket::AU,
157 "JP" => TrdMarket::JP,
158 "MY" => TrdMarket::MY,
159 "CA" => TrdMarket::CA,
160 _ => match compact.as_str() {
161 "FUTURESSIMULATEHK" => TrdMarket::FuturesSimulateHK,
162 "FUTURESSIMULATEUS" => TrdMarket::FuturesSimulateUS,
163 "FUTURESSIMULATESG" => TrdMarket::FuturesSimulateSG,
164 "FUTURESSIMULATEJP" => TrdMarket::FuturesSimulateJP,
165 "HKFUND" => TrdMarket::HKFund,
166 "USFUND" => TrdMarket::USFund,
167 "SGFUND" => TrdMarket::SGFund,
168 "MYFUND" => TrdMarket::MYFund,
169 "JPFUND" => TrdMarket::JPFund,
170 _ => return upper.parse::<i32>().ok().and_then(trd_market_from_i32),
171 },
172 };
173 Some(market)
174}
175
176#[must_use]
179pub fn trd_market_from_i32(value: i32) -> Option<TrdMarket> {
180 match value {
181 1 => Some(TrdMarket::HK),
182 2 => Some(TrdMarket::US),
183 3 => Some(TrdMarket::CN),
184 4 => Some(TrdMarket::HKCC),
185 5 => Some(TrdMarket::Futures),
186 6 => Some(TrdMarket::SG),
187 7 => Some(TrdMarket::Crypto),
188 8 => Some(TrdMarket::AU),
189 10 => Some(TrdMarket::FuturesSimulateHK),
190 11 => Some(TrdMarket::FuturesSimulateUS),
191 12 => Some(TrdMarket::FuturesSimulateSG),
192 13 => Some(TrdMarket::FuturesSimulateJP),
193 15 => Some(TrdMarket::JP),
194 111 => Some(TrdMarket::MY),
195 112 => Some(TrdMarket::CA),
196 113 => Some(TrdMarket::HKFund),
197 123 => Some(TrdMarket::USFund),
198 124 => Some(TrdMarket::SGFund),
199 125 => Some(TrdMarket::MYFund),
200 126 => Some(TrdMarket::JPFund),
201 _ => None,
202 }
203}
204
205#[must_use]
211pub fn canonical_fund_trd_market_label(market: TrdMarket) -> Option<&'static str> {
212 match market {
213 TrdMarket::HKFund => Some("HKFund"),
214 TrdMarket::USFund => Some("USFund"),
215 TrdMarket::SGFund => Some("SGFund"),
216 TrdMarket::MYFund => Some("MYFund"),
217 TrdMarket::JPFund => Some("JPFund"),
218 _ => None,
219 }
220}
221
222#[must_use]
224pub fn parse_non_fund_trd_market(raw: &str) -> Option<TrdMarket> {
225 parse_trd_market(raw).and_then(|market| {
226 if canonical_fund_trd_market_label(market).is_some() {
227 None
228 } else {
229 Some(market)
230 }
231 })
232}
233
234pub fn strip_market_prefix(code: &str) -> String {
238 const MARKET_PREFIXES: &[&str] = &[
239 "HK.", "US.", "SH.", "SZ.", "SG.", "JP.", "AU.", "CA.", "MY.", "BJ.", "CN.",
240 ];
241 for prefix in MARKET_PREFIXES {
242 if let Some(stripped) = code.strip_prefix(prefix) {
243 return stripped.to_string();
244 }
245 }
246 code.to_string()
247}
248
249pub fn derive_trd_sec_market(ftapi_sec_market: i32, trd_market: i32, code: &str) -> TrdSecMarketId {
255 if let Some(from_prefix) = sec_market_from_code_prefix_typed(code) {
256 return from_prefix;
257 }
258
259 if let Some(from_ticker) = futures_ticker_to_trd_sec_market(code) {
260 return from_ticker;
261 }
262
263 if ftapi_sec_market != 0 {
264 return TrdSecMarketId::new(ftapi_sec_market);
265 }
266
267 TrdSecMarketId::new(match trd_market {
268 1 | 4 | 113 => 1,
269 2 | 11 | 123 => 2,
270 3 => {
271 let bare = code
272 .trim_start_matches("SH.")
273 .trim_start_matches("SZ.")
274 .trim_start_matches("CN.");
275 match bare.chars().next() {
276 Some('6') | Some('9') => 31,
277 Some('0') | Some('2') | Some('3') => 32,
278 _ => 31,
279 }
280 }
281 6 | 12 | 124 => 41,
282 8 => 61,
283 15 | 13 | 126 => 51,
284 111 | 125 => 71,
285 112 => 81,
286 _ => 0,
287 })
288}
289
290pub fn derive_sec_market(ftapi_sec_market: i32, trd_market: i32, code: &str) -> i32 {
295 derive_trd_sec_market(ftapi_sec_market, trd_market, code).raw_i32()
296}
297
298pub fn sec_market_from_code_prefix(code: &str) -> Option<i32> {
300 sec_market_from_code_prefix_typed(code).map(TrdSecMarketId::raw_i32)
301}
302
303pub fn sec_market_from_code_prefix_typed(code: &str) -> Option<TrdSecMarketId> {
305 futu_core::market::entry_by_code_prefix(code).map(|e| TrdSecMarketId::new(e.sec_market))
306}
307
308#[must_use]
314pub fn trd_market_label(i: i32) -> Option<&'static str> {
315 match i {
316 1 => Some("HK"),
317 2 => Some("US"),
318 3 => Some("CN"),
319 4 => Some("HKCC"),
320 5 => Some("FUTURES"),
321 6 => Some("SG"),
322 7 => Some("CRYPTO"),
323 8 => Some("AU"),
324 10 => Some("FUTURES_SIMULATE_HK"),
325 11 => Some("FUTURES_SIMULATE_US"),
326 12 => Some("FUTURES_SIMULATE_SG"),
327 13 => Some("FUTURES_SIMULATE_JP"),
328 15 => Some("JP"),
329 111 => Some("MY"),
330 112 => Some("CA"),
331 113 => Some("HKFUND"),
332 123 => Some("USFUND"),
333 124 => Some("SGFUND"),
334 125 => Some("MYFUND"),
335 126 => Some("JPFUND"),
336 _ => None,
337 }
338}
339
340#[must_use]
349pub fn view_only_fund_market_label(trd_market: i32) -> Option<&'static str> {
350 CachedAccountMarket::new(trd_market).view_only_fund_label()
351}
352
353pub fn is_futures_code(code: &str) -> bool {
358 let code = strip_market_prefix(code);
359 let code = code.as_str();
360
361 if let Some(stem) = code
362 .strip_suffix("main")
363 .or_else(|| code.strip_suffix(".main"))
364 {
365 let stem = stem.trim_end_matches('.');
366 if (1..=4).contains(&stem.len()) && stem.chars().all(|c| c.is_ascii_alphabetic()) {
367 return true;
368 }
369 }
370
371 if (5..=8).contains(&code.len()) {
372 let len = code.len();
373 let bytes = code.as_bytes();
374 if !bytes[len - 4..].iter().all(|b| b.is_ascii_digit()) {
375 return false;
376 }
377 let ticker = &bytes[..len - 4];
378 return (1..=4).contains(&ticker.len())
379 && ticker.iter().all(|b| b.is_ascii_alphanumeric())
380 && ticker.iter().any(|b| b.is_ascii_alphabetic());
381 }
382
383 false
384}
385
386pub fn futures_ticker_to_trd_sec_market(code: &str) -> Option<TrdSecMarketId> {
388 if !is_futures_code(code) {
389 return None;
390 }
391 let ticker = extract_futures_ticker_prefix(code);
392 if matches!(
393 ticker.as_str(),
394 "NQ" | "MNQ"
395 | "ES"
396 | "MES"
397 | "RTY"
398 | "M2K"
399 | "NKD"
400 | "YM"
401 | "MYM"
402 | "6E"
403 | "6J"
404 | "6B"
405 | "6A"
406 | "6C"
407 | "6S"
408 | "6M"
409 | "6N"
410 | "GE"
411 | "SR3"
412 | "BTC"
413 | "MBT"
414 | "ETH"
415 | "MET"
416 | "CL"
417 | "MCL"
418 | "NG"
419 | "MNG"
420 | "HO"
421 | "RB"
422 | "BZ"
423 | "WBS"
424 | "GC"
425 | "MGC"
426 | "SI"
427 | "SIL"
428 | "HG"
429 | "MHG"
430 | "PL"
431 | "PA"
432 | "ZS"
433 | "ZC"
434 | "ZW"
435 | "ZO"
436 | "ZR"
437 | "ZT"
438 | "ZF"
439 | "ZN"
440 | "ZB"
441 | "UB"
442 | "TN"
443 | "VX"
444 | "VXM"
445 ) {
446 return Some(TrdSecMarketId::new(2));
447 }
448 if matches!(
449 ticker.as_str(),
450 "HSI" | "HHI" | "HTI" | "MHI" | "MCH" | "VHSI" | "CSI300" | "CSI500" | "CSI800"
451 ) {
452 return Some(TrdSecMarketId::new(1));
453 }
454 None
455}
456
457pub fn futures_ticker_to_sec_market(code: &str) -> Option<i32> {
459 futures_ticker_to_trd_sec_market(code).map(TrdSecMarketId::raw_i32)
460}
461
462pub fn extract_futures_ticker_prefix(code: &str) -> String {
464 let bare = strip_market_prefix(code);
465 let bare = bare.strip_suffix("main").unwrap_or(&bare);
466 let chars: Vec<char> = bare.chars().collect();
467 let len = chars.len();
468 if len >= 4 && chars[len - 4..].iter().all(|c| c.is_ascii_digit()) {
469 return chars[..len - 4].iter().collect::<String>().to_uppercase();
470 }
471 bare.to_uppercase()
472}
473
474#[cfg(test)]
475mod tests;