Skip to main content

futu_core/
trade_security.rs

1//! Trade-side security code shape and market derivation helpers.
2//!
3//! These pure rules sit below both SDK/runtime crates and trade-domain crates:
4//! `futu-trd` needs them for public SDK projection, while trade-write domain
5//! keeps compatibility re-exports for write-path policy.
6
7/// Input facts used to derive FTAPI `TrdSecMarket`.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct TrdSecMarketInput<'a> {
10    pub ftapi_sec_market: i32,
11    pub trd_market: i32,
12    pub code: &'a str,
13}
14
15/// Strip a known FTAPI market prefix from a user/security code.
16///
17/// Unknown dotted symbols such as `BRK.B` are preserved.
18#[must_use]
19pub fn strip_market_prefix(code: &str) -> String {
20    // Public FTAPI market prefixes only. Unknown dotted symbols, including
21    // ordinary tickers like `BRK.B`, are intentionally preserved.
22    const MARKET_PREFIXES: &[&str] = &[
23        "HK.", "US.", "SH.", "SZ.", "SG.", "JP.", "AU.", "CA.", "MY.", "BJ.", "CN.", "CRYPTO.",
24        "CC.",
25    ];
26    for prefix in MARKET_PREFIXES {
27        if let Some(stripped) = code.strip_prefix(prefix) {
28            return stripped.to_string();
29        }
30    }
31    code.to_string()
32}
33
34/// C++-aligned option symbol shape fallback.
35///
36/// Cache/static security info remains more authoritative for odd symbols; this
37/// fallback keeps prefixed and bare option symbols idempotent.
38#[must_use]
39pub fn is_option_code(code: &str) -> bool {
40    let code = strip_market_prefix(code);
41    let code = code.as_str();
42
43    if code.len() < 10 {
44        return false;
45    }
46    let has_cp_late = code
47        .char_indices()
48        .any(|(i, c)| i >= 5 && (c == 'C' || c == 'P'));
49    let has_digit = code.chars().any(|c| c.is_ascii_digit());
50    let has_alpha = code.chars().any(|c| c.is_ascii_alphabetic());
51    has_cp_late && has_digit && has_alpha
52}
53
54/// HK index option ticker fallback used when static `mkt_id` is unavailable.
55#[must_use]
56pub fn is_hk_index_option_code(code: &str) -> bool {
57    let code = strip_market_prefix(code);
58    let code = code.as_str();
59    let prefix: String = code
60        .chars()
61        .take_while(|c| c.is_ascii_alphabetic())
62        .collect();
63    matches!(
64        prefix.to_uppercase().as_str(),
65        "HSI" | "HHI" | "HTI" | "MHI" | "MCH"
66    )
67}
68
69/// Return whether the code looks like a futures symbol.
70///
71/// This is only the cache-miss fallback. Static security info remains more
72/// authoritative when available.
73#[must_use]
74pub fn is_futures_code(code: &str) -> bool {
75    let code = strip_market_prefix(code);
76    let code = code.as_str();
77
78    if let Some(stem) = code
79        .strip_suffix("main")
80        .or_else(|| code.strip_suffix(".main"))
81    {
82        let stem = stem.trim_end_matches('.');
83        if (1..=4).contains(&stem.len()) && stem.chars().all(|c| c.is_ascii_alphabetic()) {
84            return true;
85        }
86    }
87
88    if (5..=8).contains(&code.len()) {
89        let len = code.len();
90        let bytes = code.as_bytes();
91        if !bytes[len - 4..].iter().all(|b| b.is_ascii_digit()) {
92            return false;
93        }
94        let ticker = &bytes[..len - 4];
95        return (1..=4).contains(&ticker.len())
96            && ticker.iter().all(|b| b.is_ascii_alphanumeric())
97            && ticker.iter().any(|b| b.is_ascii_alphabetic());
98    }
99
100    false
101}
102
103/// Extract the ticker prefix from a futures code.
104#[must_use]
105pub fn extract_futures_ticker_prefix_like_cpp(code: &str) -> String {
106    let bare = strip_market_prefix(code);
107    let bare = bare.strip_suffix("main").unwrap_or(&bare);
108    let chars: Vec<char> = bare.chars().collect();
109    let len = chars.len();
110    if len >= 4 && chars[len - 4..].iter().all(|c| c.is_ascii_digit()) {
111        return chars[..len - 4].iter().collect::<String>().to_uppercase();
112    }
113    bare.to_uppercase()
114}
115
116/// Derive FTAPI `TrdSecMarket` from an explicit code prefix.
117#[must_use]
118pub fn sec_market_from_code_prefix_like_cpp(code: &str) -> Option<i32> {
119    crate::market::entry_by_code_prefix(code).map(|entry| entry.sec_market)
120}
121
122/// Derive `TrdSecMarket` from known futures ticker prefixes.
123///
124/// This is a cache-miss fallback for FTAPI inputs whose SDK market metadata is
125/// stale or too generic. Prefix and static/cache facts remain more
126/// authoritative when available.
127#[must_use]
128pub fn futures_ticker_to_sec_market_like_cpp(code: &str) -> Option<i32> {
129    if !is_futures_code(code) {
130        return None;
131    }
132    let ticker = extract_futures_ticker_prefix_like_cpp(code);
133    if matches!(
134        ticker.as_str(),
135        "NQ" | "MNQ"
136            | "ES"
137            | "MES"
138            | "RTY"
139            | "M2K"
140            | "NKD"
141            | "YM"
142            | "MYM"
143            | "6E"
144            | "6J"
145            | "6B"
146            | "6A"
147            | "6C"
148            | "6S"
149            | "6M"
150            | "6N"
151            | "GE"
152            | "SR3"
153            | "BTC"
154            | "MBT"
155            | "ETH"
156            | "MET"
157            | "CL"
158            | "MCL"
159            | "NG"
160            | "MNG"
161            | "HO"
162            | "RB"
163            | "BZ"
164            | "WBS"
165            | "GC"
166            | "MGC"
167            | "SI"
168            | "SIL"
169            | "HG"
170            | "MHG"
171            | "PL"
172            | "PA"
173            | "ZS"
174            | "ZC"
175            | "ZW"
176            | "ZO"
177            | "ZR"
178            | "ZT"
179            | "ZF"
180            | "ZN"
181            | "ZB"
182            | "UB"
183            | "TN"
184            | "VX"
185            | "VXM"
186    ) {
187        return Some(2);
188    }
189    if matches!(
190        ticker.as_str(),
191        "HSI" | "HHI" | "HTI" | "MHI" | "MCH" | "VHSI" | "CSI300" | "CSI500" | "CSI800"
192    ) {
193        return Some(1);
194    }
195    None
196}
197
198/// C++-aligned `TrdSecMarket` derivation for trade requests.
199///
200/// Prefix and futures ticker fallback intentionally win over SDK supplied
201/// market, matching the established v1.4.56 behavior for futures symbols where
202/// client SDK market metadata can be stale or too generic.
203#[must_use]
204pub fn derive_trd_sec_market_like_cpp(input: TrdSecMarketInput<'_>) -> i32 {
205    if let Some(from_prefix) = sec_market_from_code_prefix_like_cpp(input.code) {
206        return from_prefix;
207    }
208
209    if let Some(from_ticker) = futures_ticker_to_sec_market_like_cpp(input.code) {
210        return from_ticker;
211    }
212
213    if input.ftapi_sec_market != 0 {
214        return input.ftapi_sec_market;
215    }
216
217    match input.trd_market {
218        // Public TrdMarket 10 is Futures_Simulate_HK and maps to HK just as
219        // raw NN_TrdMarket 10 does.
220        // Ref: `_APIServer_Trd_Comm.cpp:1687-1694` `GetTrdSecMarket`.
221        1 | 4 | 10 | 113 => 1,
222        2 | 11 | 123 => 2,
223        3 => {
224            let bare = input
225                .code
226                .trim_start_matches("SH.")
227                .trim_start_matches("SZ.")
228                .trim_start_matches("CN.");
229            match bare.chars().next() {
230                Some('6') | Some('9') => 31,
231                Some('0') | Some('2') | Some('3') => 32,
232                _ => 31,
233            }
234        }
235        6 | 12 | 124 => 41,
236        // Ref: Trd_Common.proto `TrdMarket_Crypto=7` and
237        // `TrdSecMarket_CC=101`; C++ `GetTrdSecMarket` returns CC for
238        // `NN_TrdMarket_Crypto` (`_APIServer_Trd_Comm.cpp:1769`).
239        7 => 101,
240        8 => 61,
241        15 | 13 | 126 => 51,
242        // C++ `_APIServer_Trd_Comm.cpp:2044-2047`: Prediction requires
243        // public `TrdSecMarket_EC` (111).
244        17 => 111,
245        111 | 125 => 71,
246        112 => 81,
247        _ => 0,
248    }
249}