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//! | 其他/Unknown | — | Asia/Hong_Kong (fallback 零回归) | China default |
27
28/// FTAPI `Qot_Common.QotMarket` domain.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct QotMarketId(i32);
31
32impl QotMarketId {
33    #[must_use]
34    pub const fn new(raw: i32) -> Self {
35        Self(raw)
36    }
37
38    #[must_use]
39    pub const fn raw_i32(self) -> i32 {
40        self.0
41    }
42}
43
44/// FTAPI `Trd_Common.TrdSecMarket` domain.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct TrdSecMarketId(i32);
47
48impl TrdSecMarketId {
49    #[must_use]
50    pub const fn new(raw: i32) -> Self {
51        Self(raw)
52    }
53
54    #[must_use]
55    pub const fn raw_i32(self) -> i32 {
56        self.0
57    }
58}
59
60/// Backend `NN_QuoteMktID` / mkt_id domain.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct BackendMktId(u32);
63
64impl BackendMktId {
65    #[must_use]
66    pub const fn new(raw: u32) -> Self {
67        Self(raw)
68    }
69
70    #[must_use]
71    pub const fn raw_u32(self) -> u32 {
72        self.0
73    }
74}
75
76/// **Stable API** (since v1.4.69) — 市场 dispatch 表 entry(跨 crate 共享)。
77///
78/// 合并 3 个 fn 的重复 match case:
79/// - `sec_market_from_code_prefix`(prefix → sec_market)
80/// - `sec_market_to_qot_market`(sec_market → qot_market)
81/// - `sec_market_to_exchange_str`(sec_market → exchange_str)
82///
83/// 每 entry 覆盖一个 market family(9 个标准市场)。新加市场只需加表一行,
84/// 3 个 helper 自动支持(vs 之前 3 处独立 match 容易漏 case)。
85///
86/// **CN 特例**:CN prefix 没有标 sec_market(由 code 首数字判 SH(31) vs
87/// SZ(32)),因此 `prefix == "CN."` 的 entry 不放表里,`sec_market_from_code_prefix`
88/// 对 `CN.` 单独处理。
89pub struct MarketDispatchEntry {
90    pub prefix: &'static str,
91    pub sec_market: i32,
92    pub qot_market: u32,
93    pub exchange_str: &'static str,
94}
95
96/// **Stable API** (since v1.4.69) — 9 个标准市场 dispatch 表常量。
97///
98/// 对齐 C++ `_NNProto_Trd_Comm.cpp::GetStockExchangeByMktID` + FTAPI
99/// QotMarket/TrdSecMarket proto 枚举。
100pub const MARKET_DISPATCH: &[MarketDispatchEntry] = &[
101    MarketDispatchEntry {
102        prefix: "HK.",
103        sec_market: 1,
104        qot_market: 1,
105        exchange_str: "SEHK",
106    },
107    MarketDispatchEntry {
108        prefix: "US.",
109        sec_market: 2,
110        qot_market: 11,
111        exchange_str: "US",
112    },
113    MarketDispatchEntry {
114        prefix: "SH.",
115        sec_market: 31,
116        qot_market: 21,
117        exchange_str: "SSE",
118    },
119    MarketDispatchEntry {
120        prefix: "SZ.",
121        sec_market: 32,
122        qot_market: 22,
123        exchange_str: "SZSE",
124    },
125    MarketDispatchEntry {
126        prefix: "SG.",
127        sec_market: 41,
128        qot_market: 31,
129        exchange_str: "SGX",
130    },
131    MarketDispatchEntry {
132        prefix: "JP.",
133        sec_market: 51,
134        qot_market: 41,
135        exchange_str: "TSE",
136    },
137    MarketDispatchEntry {
138        prefix: "AU.",
139        sec_market: 61,
140        qot_market: 51,
141        exchange_str: "ASX",
142    },
143    MarketDispatchEntry {
144        prefix: "MY.",
145        sec_market: 71,
146        qot_market: 61,
147        exchange_str: "BURSA",
148    },
149    MarketDispatchEntry {
150        prefix: "CA.",
151        sec_market: 81,
152        qot_market: 71,
153        exchange_str: "TSX",
154    },
155];
156
157/// **Stable API** (since v1.4.69) — 查 sec_market 对应的 entry(O(n),n=9 常数时间)。
158pub fn entry_by_sec_market(sec_market: i32) -> Option<&'static MarketDispatchEntry> {
159    entry_by_trd_sec_market_id(TrdSecMarketId::new(sec_market))
160}
161
162/// Typed lookup for `TrdSecMarketId` → market dispatch entry.
163pub fn entry_by_trd_sec_market_id(
164    sec_market: TrdSecMarketId,
165) -> Option<&'static MarketDispatchEntry> {
166    MARKET_DISPATCH
167        .iter()
168        .find(|e| e.sec_market == sec_market.raw_i32())
169}
170
171/// Typed lookup for `QotMarketId` → market dispatch entry.
172pub fn entry_by_qot_market_id(qot_market: QotMarketId) -> Option<&'static MarketDispatchEntry> {
173    MARKET_DISPATCH
174        .iter()
175        .find(|e| e.qot_market as i32 == qot_market.raw_i32())
176}
177
178/// User-facing display prefix for public `Qot_Common.QotMarket` values.
179///
180/// Spot/security markets come from [`MARKET_DISPATCH`] so new market families
181/// are not re-hardcoded per surface. `HK_FUTURE`, `FX`, and `CC` are public
182/// QotMarket values that do not map to the stock dispatch table.
183#[must_use]
184pub fn qot_market_display_prefix(qot_market: QotMarketId) -> Option<&'static str> {
185    if let Some(entry) = entry_by_qot_market_id(qot_market) {
186        return Some(entry.prefix.strip_suffix('.').unwrap_or(entry.prefix));
187    }
188
189    match qot_market.raw_i32() {
190        2 => Some("HK_FUTURE"),
191        81 => Some("FX"),
192        91 => Some("CC"),
193        _ => None,
194    }
195}
196
197/// Typed conversion from `TrdSecMarketId` to public `QotMarketId`.
198pub fn trd_sec_market_to_qot_market_id(sec_market: TrdSecMarketId) -> Option<QotMarketId> {
199    entry_by_trd_sec_market_id(sec_market).map(|e| QotMarketId::new(e.qot_market as i32))
200}
201
202/// **Stable API** (since v1.4.69) — 查 code prefix 对应的 entry(含 `CN.` 特殊分派)。
203///
204/// `CN.` 按 code 首数字判 SH(31) vs SZ(32):
205/// - `6` / `9` → SH entry (sec_market=31)
206/// - `0` / `2` / `3` → SZ entry (sec_market=32)
207/// - 其他 → SH entry(default)
208pub fn entry_by_code_prefix(code: &str) -> Option<&'static MarketDispatchEntry> {
209    // CN. 特殊分派(SH/SZ by first digit)
210    if let Some(bare) = code.strip_prefix("CN.") {
211        let sec_market = match bare.chars().next() {
212            Some('6') | Some('9') => 31,
213            Some('0') | Some('2') | Some('3') => 32,
214            _ => 31, // default SH
215        };
216        return entry_by_sec_market(sec_market);
217    }
218    MARKET_DISPATCH.iter().find(|e| code.starts_with(e.prefix))
219}
220
221/// **Stable API** (since v1.4.69) — FTAPI QotMarket 值 → IANA 时区 dispatch。
222///
223/// 已知未覆盖:FX_Security / 未知 market 值 → HKT fallback 保证零回归。
224#[must_use]
225pub fn qot_market_to_tz(market: i32) -> chrono_tz::Tz {
226    qot_market_id_to_tz(QotMarketId::new(market))
227}
228
229/// Typed variant of [`qot_market_to_tz`].
230#[must_use]
231pub fn qot_market_id_to_tz(market: QotMarketId) -> chrono_tz::Tz {
232    match market.raw_i32() {
233        1 | 2 | 21 | 22 => chrono_tz::Asia::Hong_Kong, // HK / HK_Future / CNSH / CNSZ
234        11 => chrono_tz::America::New_York,            // US (DST-aware)
235        31 => chrono_tz::Asia::Singapore,              // SG
236        41 => chrono_tz::Asia::Tokyo,                  // JP
237        51 => chrono_tz::Australia::Sydney,            // AU (DST-aware)
238        61 => chrono_tz::Asia::Kuala_Lumpur,           // MY
239        71 => chrono_tz::America::Toronto,             // CA (DST-aware)
240        _ => chrono_tz::Asia::Hong_Kong,               // 未知 fallback HKT
241    }
242}
243
244/// **Stable API** (since v1.4.71) — FTAPI `TrdMarket` 值 → IANA 时区 dispatch。
245///
246/// **对齐 C++ `GetTimeZoneByTrdMkt`**(`APIServer_Inner_API.cpp:4022`),用于:
247/// 1. 用户传的 `begin_time` / `end_time` 字符串解析(C++ `APITimeStrToTimeStamp_Trd`)
248/// 2. backend 返回的时间戳 → market local 时间字符串(C++ `TimeStampToAPITimeStr_Trd`)
249/// 3. 历史查询 default time range fallback(查 US 账户 "最近 90 天" 应按 US tz 算)
250///
251/// | FTAPI TrdMarket | 值 | 时区 | C++ `E_StandardTime` |
252/// |---|---|---|---|
253/// | HK / CN / HKCC | 1/3/4 | Asia/Hong_Kong (UTC+8) | China |
254/// | US | 2 | America/New_York (DST-aware EDT/EST) | USEastern |
255/// | SG | 6 | Asia/Singapore (UTC+8) | SG |
256/// | AU | 8 | Australia/Sydney (DST-aware AEDT/AEST) | AU |
257/// | JP | 15 | Asia/Tokyo (UTC+9) | JP |
258/// | MY | 111 | Asia/Kuala_Lumpur (UTC+8) | MY |
259/// | CA | 112 | America/Toronto (DST-aware EDT/EST) | CA |
260/// | Futures (5) / Unknown (0) | 5/0/其他 | Asia/Hong_Kong fallback | China |
261///
262/// **Futures 细分限制**:C++ Futures 按 `mkt_id` (CME→US/NYMEX→US/SGX→SG/HKFE→HK)
263/// 细分,Rust 当前 order/orderfill struct **没有** `mkt_id` 上下文,只能 fallback
264/// HKT。未来从 quote cache pass `mkt_id` 时再扩(`CachedSecurityInfo.mkt_id` 已有
265/// v1.4.59 加的字段,待 Phase D 全量 wire)。
266#[must_use]
267pub fn trd_market_to_tz(trd_market: i32) -> chrono_tz::Tz {
268    match trd_market {
269        1 | 3 | 4 => chrono_tz::Asia::Hong_Kong, // HK / CN / HKCC
270        2 => chrono_tz::America::New_York,       // US (DST-aware)
271        6 => chrono_tz::Asia::Singapore,         // SG
272        8 => chrono_tz::Australia::Sydney,       // AU (DST-aware)
273        15 => chrono_tz::Asia::Tokyo,            // JP
274        111 => chrono_tz::Asia::Kuala_Lumpur,    // MY
275        112 => chrono_tz::America::Toronto,      // CA (DST-aware)
276        // Futures (5) / Unknown (0) / 未映射:fallback HKT
277        // C++ Futures 按 mkt_id 细分,Rust struct 无此上下文 → 保守 HKT
278        _ => chrono_tz::Asia::Hong_Kong,
279    }
280}
281
282#[cfg(test)]
283mod tests;