Skip to main content

futu_core/
market.rs

1//! 市场 enum 与时区 dispatch(v1.4.69 跨 crate 共享)
2//!
3//! v1.4.68 以前 `bridge/utils.rs` 和 `qot/util.rs` 各维护一份 `qot_market_to_tz`
4//! 实装(L40 一行 match)。注释约定 "修改需同步 sync"。v1.4.69 合并到
5//! `futu-core::market`,两个调用方都 `use futu_core::market::qot_market_to_tz`
6//! 避免重复维护。
7//!
8//! 对齐 C++ `APIServer_Inner_API.cpp::GetTimeZoneByAPIQotMkt` (L3684) — 按
9//! FTAPI `QotMarket` enum 值分 IANA 时区。
10//!
11//! **映射表**:
12//!
13//! | FTAPI QotMarket | 值 | IANA Tz | C++ E_StandardTime |
14//! |---|---|---|---|
15//! | HK_Security | 1 | Asia/Hong_Kong | China |
16//! | HK_Future (deprecated) | 2 | Asia/Hong_Kong | China |
17//! | US_Security | 11 | America/New_York (DST) | USEastern |
18//! | CNSH_Security | 21 | Asia/Hong_Kong | China |
19//! | CNSZ_Security | 22 | Asia/Hong_Kong | China |
20//! | SG_Security | 31 | Asia/Singapore | SG |
21//! | JP_Security | 41 | Asia/Tokyo | JP |
22//! | AU_Security | 51 | Australia/Sydney (DST) | AU |
23//! | MY_Security | 61 | Asia/Kuala_Lumpur | MY |
24//! | CA_Security | 71 | America/Toronto (DST) | CA |
25//! | FX_Security | 81 | Asia/Hong_Kong (fallback) | — |
26//! | CC_Security | 91 | America/New_York (DST) | USEastern |
27//! | EventContract | 101 | America/New_York (DST) | USEastern |
28//! | 其他/Unknown | — | Asia/Hong_Kong (fallback 零回归) | China default |
29
30/// FTAPI `Qot_Common.QotMarket` domain.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct QotMarketId(i32);
33
34impl QotMarketId {
35    #[must_use]
36    pub const fn new(raw: i32) -> Self {
37        Self(raw)
38    }
39
40    #[must_use]
41    pub const fn raw_i32(self) -> i32 {
42        self.0
43    }
44}
45
46/// FTAPI `Trd_Common.TrdSecMarket` domain.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub struct TrdSecMarketId(i32);
49
50impl TrdSecMarketId {
51    #[must_use]
52    pub const fn new(raw: i32) -> Self {
53        Self(raw)
54    }
55
56    #[must_use]
57    pub const fn raw_i32(self) -> i32 {
58        self.0
59    }
60}
61
62/// Backend `NN_QuoteMktID` / mkt_id domain.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct BackendMktId(u32);
65
66impl BackendMktId {
67    #[must_use]
68    pub const fn new(raw: u32) -> Self {
69        Self(raw)
70    }
71
72    #[must_use]
73    pub const fn raw_u32(self) -> u32 {
74        self.0
75    }
76}
77
78/// **Stable API** (since v1.4.69) — 市场 dispatch 表 entry(跨 crate 共享)。
79///
80/// 合并 3 个 fn 的重复 match case:
81/// - `sec_market_from_code_prefix`(prefix → sec_market)
82/// - `sec_market_to_qot_market`(sec_market → qot_market)
83/// - `sec_market_to_exchange_str`(sec_market → exchange_str)
84///
85/// 每 entry 覆盖一个 market family(9 个标准市场)。新加市场只需加表一行,
86/// 3 个 helper 自动支持(vs 之前 3 处独立 match 容易漏 case)。
87///
88/// **CN 特例**:CN prefix 没有标 sec_market(由 code 首数字判 SH(31) vs
89/// SZ(32)),因此 `prefix == "CN."` 的 entry 不放表里,`sec_market_from_code_prefix`
90/// 对 `CN.` 单独处理。
91pub struct MarketDispatchEntry {
92    pub prefix: &'static str,
93    pub sec_market: i32,
94    pub qot_market: u32,
95    pub exchange_str: &'static str,
96}
97
98/// **Stable API** (since v1.4.69) — 9 个标准市场 dispatch 表常量。
99///
100/// 对齐 C++ `_NNProto_Trd_Comm.cpp::GetStockExchangeByMktID` + FTAPI
101/// QotMarket/TrdSecMarket proto 枚举。
102pub const MARKET_DISPATCH: &[MarketDispatchEntry] = &[
103    MarketDispatchEntry {
104        prefix: "HK.",
105        sec_market: 1,
106        qot_market: 1,
107        exchange_str: "SEHK",
108    },
109    MarketDispatchEntry {
110        prefix: "US.",
111        sec_market: 2,
112        qot_market: 11,
113        exchange_str: "US",
114    },
115    MarketDispatchEntry {
116        prefix: "SH.",
117        sec_market: 31,
118        qot_market: 21,
119        exchange_str: "SSE",
120    },
121    MarketDispatchEntry {
122        prefix: "SZ.",
123        sec_market: 32,
124        qot_market: 22,
125        exchange_str: "SZSE",
126    },
127    MarketDispatchEntry {
128        prefix: "SG.",
129        sec_market: 41,
130        qot_market: 31,
131        exchange_str: "SGX",
132    },
133    MarketDispatchEntry {
134        prefix: "JP.",
135        sec_market: 51,
136        qot_market: 41,
137        exchange_str: "TSE",
138    },
139    MarketDispatchEntry {
140        prefix: "AU.",
141        sec_market: 61,
142        qot_market: 51,
143        exchange_str: "ASX",
144    },
145    MarketDispatchEntry {
146        prefix: "MY.",
147        sec_market: 71,
148        qot_market: 61,
149        exchange_str: "BURSA",
150    },
151    MarketDispatchEntry {
152        prefix: "CA.",
153        sec_market: 81,
154        qot_market: 71,
155        exchange_str: "TSX",
156    },
157];
158
159/// **Stable API** (since v1.4.69) — 查 sec_market 对应的 entry(O(n),n=9 常数时间)。
160pub fn entry_by_sec_market(sec_market: i32) -> Option<&'static MarketDispatchEntry> {
161    entry_by_trd_sec_market_id(TrdSecMarketId::new(sec_market))
162}
163
164/// Typed lookup for `TrdSecMarketId` → market dispatch entry.
165pub fn entry_by_trd_sec_market_id(
166    sec_market: TrdSecMarketId,
167) -> Option<&'static MarketDispatchEntry> {
168    MARKET_DISPATCH
169        .iter()
170        .find(|e| e.sec_market == sec_market.raw_i32())
171}
172
173/// Typed lookup for `QotMarketId` → market dispatch entry.
174pub fn entry_by_qot_market_id(qot_market: QotMarketId) -> Option<&'static MarketDispatchEntry> {
175    MARKET_DISPATCH
176        .iter()
177        .find(|e| e.qot_market as i32 == qot_market.raw_i32())
178}
179
180/// User-facing display prefix for public `Qot_Common.QotMarket` values.
181///
182/// Spot/security markets come from [`MARKET_DISPATCH`] so new market families
183/// are not re-hardcoded per surface. `HK_FUTURE`, `FX`, and `CC` are public
184/// QotMarket values that do not map to the stock dispatch table.
185#[must_use]
186pub fn qot_market_display_prefix(qot_market: QotMarketId) -> Option<&'static str> {
187    if let Some(entry) = entry_by_qot_market_id(qot_market) {
188        return Some(entry.prefix.strip_suffix('.').unwrap_or(entry.prefix));
189    }
190
191    match qot_market.raw_i32() {
192        2 => Some("HK_FUTURE"),
193        81 => Some("FX"),
194        91 => Some("CC"),
195        // Ref: APIServer_Inner_API.cpp:5306-5307 GetMarketCodePrefix.
196        101 => Some("EC"),
197        _ => None,
198    }
199}
200
201/// `Qot_Common.QotMarketState` enum -> user-facing diagnostic label.
202///
203/// Strictly follows `proto/Qot_Common.proto` market-state values, including
204/// night/overnight/futures states. This is display-only: protocol handlers
205/// must keep forwarding the original numeric value.
206#[must_use]
207pub const fn qot_market_state_label(state: i32) -> &'static str {
208    match state {
209        0 => "None",
210        1 => "Auction",
211        2 => "WaitingOpen",
212        3 => "Morning",
213        4 => "Rest",
214        5 => "Afternoon",
215        6 => "Closed",
216        8 => "PreMarketBegin",
217        9 => "PreMarketEnd",
218        10 => "AfterHoursBegin",
219        11 => "AfterHoursEnd",
220        12 => "FutuSwitchDate",
221        13 => "NightOpen",
222        14 => "NightEnd",
223        15 => "FutureDayOpen",
224        16 => "FutureDayBreak",
225        17 => "FutureDayClose",
226        18 => "FutureDayWaitForOpen",
227        19 => "HkCas",
228        20 => "FutureNightWait",
229        21 => "FutureAfternoon",
230        22 => "FutureSwitchDate",
231        23 => "FutureOpen",
232        24 => "FutureBreak",
233        25 => "FutureBreakOver",
234        26 => "FutureClose",
235        27 => "StibAfterHoursWait",
236        28 => "StibAfterHoursBegin",
237        29 => "StibAfterHoursEnd",
238        30 => "CloseAuction",
239        31 => "AfternoonEnd",
240        32 => "Night",
241        33 => "OvernightBegin",
242        34 => "OvernightEnd",
243        35 => "TradeAtLast",
244        36 => "TradeAuction",
245        37 => "Overnight",
246        _ => "Unknown",
247    }
248}
249
250/// Typed conversion from `TrdSecMarketId` to public `QotMarketId`.
251pub fn trd_sec_market_to_qot_market_id(sec_market: TrdSecMarketId) -> Option<QotMarketId> {
252    entry_by_trd_sec_market_id(sec_market).map(|e| QotMarketId::new(e.qot_market as i32))
253}
254
255/// **Stable API** (since v1.4.69) — 查 code prefix 对应的 entry(含 `CN.` 特殊分派)。
256///
257/// `CN.` 按 code 首数字判 SH(31) vs SZ(32):
258/// - `6` / `9` → SH entry (sec_market=31)
259/// - `0` / `2` / `3` → SZ entry (sec_market=32)
260/// - 其他 → SH entry(default)
261pub fn entry_by_code_prefix(code: &str) -> Option<&'static MarketDispatchEntry> {
262    // CN. 特殊分派(SH/SZ by first digit)
263    if let Some(bare) = code.strip_prefix("CN.") {
264        let sec_market = match bare.chars().next() {
265            Some('6') | Some('9') => 31,
266            Some('0') | Some('2') | Some('3') => 32,
267            _ => 31, // default SH
268        };
269        return entry_by_sec_market(sec_market);
270    }
271    MARKET_DISPATCH.iter().find(|e| code.starts_with(e.prefix))
272}
273
274/// **Stable API** (since v1.4.69) — FTAPI QotMarket 值 → IANA 时区 dispatch。
275///
276/// 已知未覆盖:FX_Security / 未知 market 值 → HKT fallback 保证零回归。
277#[must_use]
278pub fn qot_market_to_tz(market: i32) -> chrono_tz::Tz {
279    qot_market_id_to_tz(QotMarketId::new(market))
280}
281
282/// Typed variant of [`qot_market_to_tz`].
283#[must_use]
284pub fn qot_market_id_to_tz(market: QotMarketId) -> chrono_tz::Tz {
285    match market.raw_i32() {
286        1 | 2 | 21 | 22 => chrono_tz::Asia::Hong_Kong, // HK / HK_Future / CNSH / CNSZ
287        11 => chrono_tz::America::New_York,            // US (DST-aware)
288        31 => chrono_tz::Asia::Singapore,              // SG
289        41 => chrono_tz::Asia::Tokyo,                  // JP
290        51 => chrono_tz::Australia::Sydney,            // AU (DST-aware)
291        61 => chrono_tz::Asia::Kuala_Lumpur,           // MY
292        71 => chrono_tz::America::Toronto,             // CA (DST-aware)
293        // Ref: APIServer_Inner_API.cpp:5699-5704. Both crypto and event
294        // contracts use E_StandardTime_USEastern in the C++ runtime.
295        91 | 101 => chrono_tz::America::New_York,
296        _ => chrono_tz::Asia::Hong_Kong, // 未知 fallback HKT
297    }
298}
299
300/// **Stable API** (since v1.4.71) — FTAPI `TrdMarket` 值 → IANA 时区 dispatch。
301///
302/// **对齐 C++ `GetTimeZoneByTrdMkt`**(`APIServer_Inner_API.cpp:4022`),用于:
303/// 1. 用户传的 `begin_time` / `end_time` 字符串解析(C++ `APITimeStrToTimeStamp_Trd`)
304/// 2. backend 返回的时间戳 → market local 时间字符串(C++ `TimeStampToAPITimeStr_Trd`)
305/// 3. 历史查询 default time range fallback(查 US 账户 "最近 90 天" 应按 US tz 算)
306///
307/// | FTAPI TrdMarket | 值 | 时区 | C++ `E_StandardTime` |
308/// |---|---|---|---|
309/// | HK / CN / HKCC | 1/3/4 | Asia/Hong_Kong (UTC+8) | China |
310/// | US | 2 | America/New_York (DST-aware EDT/EST) | USEastern |
311/// | SG | 6 | Asia/Singapore (UTC+8) | SG |
312/// | AU | 8 | Australia/Sydney (DST-aware AEDT/AEST) | AU |
313/// | JP | 15 | Asia/Tokyo (UTC+9) | JP |
314/// | MY | 111 | Asia/Kuala_Lumpur (UTC+8) | MY |
315/// | CA | 112 | America/Toronto (DST-aware EDT/EST) | CA |
316/// | Futures (5) / Unknown (0) | 5/0/其他 | Asia/Hong_Kong fallback | China |
317///
318/// Futures/Prediction callers that possess backend `mkt_id` must use
319/// [`trd_market_with_mkt_id_to_tz_like_cpp`]. This market-only helper keeps the
320/// C++ China/HK fallback for callers without that sub-exchange identity.
321#[must_use]
322pub fn trd_market_to_tz(trd_market: i32) -> chrono_tz::Tz {
323    match trd_market {
324        1 | 3 | 4 => chrono_tz::Asia::Hong_Kong, // HK / CN / HKCC
325        2 => chrono_tz::America::New_York,       // US (DST-aware)
326        6 => chrono_tz::Asia::Singapore,         // SG
327        8 => chrono_tz::Australia::Sydney,       // AU (DST-aware)
328        15 => chrono_tz::Asia::Tokyo,            // JP
329        111 => chrono_tz::Asia::Kuala_Lumpur,    // MY
330        112 => chrono_tz::America::Toronto,      // CA (DST-aware)
331        // Futures (5) / Unknown (0) / 未映射:fallback HKT
332        // C++ Futures 按 mkt_id 细分,Rust struct 无此上下文 → 保守 HKT
333        _ => chrono_tz::Asia::Hong_Kong,
334    }
335}
336
337/// Resolve a trade timestamp timezone from the FTAPI/NN trade market plus the
338/// backend quote-market identity exactly like C++ `GetTimeZoneByTrdMkt`.
339///
340/// The numeric ranges below are protocol enum constants, not server-configured
341/// routing data. Ref: `APIServer_Inner_API.cpp:5757-5882` and
342/// `NNBase_Define_Enum.h:174-177,874-903,952-979,1026-1031`. They must be
343/// revisited if those upstream enum ranges or `GetFutureStandardTime` change.
344#[must_use]
345pub fn trd_market_with_mkt_id_to_tz_like_cpp(
346    trd_market: Option<i32>,
347    mkt_id: Option<u32>,
348) -> chrono_tz::Tz {
349    // Ref: `NNBase_Define_Enum.h:174-177` defines the literal 10/11/12/13
350    // identities; `APIServer_Inner_API.cpp:5794-5801,5836-5879` defines their
351    // timezone behavior. These are fixed protocol constants, not dynamic
352    // server data; replace this dispatch only if the upstream NN_TrdMarket
353    // enum changes.
354    match trd_market.unwrap_or(0) {
355        10 => return chrono_tz::Asia::Hong_Kong,
356        11 => return chrono_tz::America::New_York,
357        12 => return chrono_tz::Asia::Singapore,
358        13 => return chrono_tz::Asia::Tokyo,
359        _ => {}
360    }
361
362    // Ref: `APIServer_Inner_API.cpp:5760-5786,5816-5819,5876-5879`.
363    // Futures=5 and Prediction=17 deliberately share the backend mkt-id table.
364    if matches!(trd_market, Some(5 | 17)) {
365        return match mkt_id {
366            Some(60..=79) => chrono_tz::America::New_York,
367            Some(80..=109) => chrono_tz::America::Chicago,
368            Some(160..=179) => chrono_tz::Asia::Singapore,
369            Some(185..=194 | 800..=849) => chrono_tz::Asia::Tokyo,
370            _ => chrono_tz::Asia::Hong_Kong,
371        };
372    }
373
374    trd_market_to_tz(trd_market.unwrap_or(0))
375}
376
377#[cfg(test)]
378mod tests;