Skip to main content

futu_core/
qot_symbol.rs

1//! Pure QOT `MARKET.CODE` symbol parsing and formatting rules.
2
3use std::fmt;
4
5use crate::market::{QotMarketId, qot_market_display_prefix};
6
7const SUPPORTED_SYMBOL_MARKETS: &str =
8    "HK, HK_FUTURE, US, SH, SZ, SG, JP, AU, MY, CA, FX, CRYPTO, CC, EC";
9
10/// Defensive user-input cap for `MARKET.CODE` shorthand strings.
11///
12/// C++ stores market-state and trade code buffers in fixed char arrays
13/// (`charArr64` / `szCode[32]` in the audited paths). Keep Rust's public
14/// shorthand cap comfortably above all observed legitimate derivatives, while
15/// rejecting pathological payloads before REST/MCP/CLI normalization allocates
16/// or forwards them.
17pub const MAX_SYMBOL_LEN: usize = 160;
18pub const MAX_SYMBOL_CODE_LEN: usize = 128;
19
20/// Exact POSIX ERE used by C++ `IsOptionCode`.
21///
22/// Ref: `NNBase_Define_Inline.h:241-263`:
23/// `^[\._0-9A-Za-z]+[0-9]{6}[PpCc][0-9]{2,}$`.
24#[must_use]
25pub fn looks_like_cpp_option_code(code: &str) -> bool {
26    let bytes = code.as_bytes();
27    if bytes.len() < 10 || !bytes.iter().all(u8::is_ascii) {
28        return false;
29    }
30    (7..bytes.len().saturating_sub(1)).any(|marker| {
31        let prefix_end = marker - 6;
32        prefix_end > 0
33            && matches!(bytes[marker], b'P' | b'p' | b'C' | b'c')
34            && bytes[..prefix_end]
35                .iter()
36                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_'))
37            && bytes[prefix_end..marker].iter().all(u8::is_ascii_digit)
38            && bytes[marker + 1..].len() >= 2
39            && bytes[marker + 1..].iter().all(u8::is_ascii_digit)
40    })
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParsedQotSymbol {
45    pub market: i32,
46    pub code: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum QotSymbolParseError {
51    SymbolTooLong { len: usize },
52    MissingSeparator { raw: String },
53    EmptyCode { raw: String },
54    CodeTooLong { len: usize },
55    UnknownMarket { raw: String, market: String },
56}
57
58impl fmt::Display for QotSymbolParseError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::SymbolTooLong { len } => write!(
62                f,
63                "invalid symbol: symbol length {len} exceeds max {MAX_SYMBOL_LEN}"
64            ),
65            Self::MissingSeparator { raw } => write!(
66                f,
67                "invalid symbol {raw:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)"
68            ),
69            Self::EmptyCode { raw } => {
70                write!(f, "invalid symbol {raw:?}: code part is empty")
71            }
72            Self::CodeTooLong { len } => write!(
73                f,
74                "invalid symbol code: code length {len} exceeds max {MAX_SYMBOL_CODE_LEN}"
75            ),
76            Self::UnknownMarket { raw, market } => write!(
77                f,
78                "invalid symbol {raw:?}: unknown market {market:?} (supported: {SUPPORTED_SYMBOL_MARKETS})"
79            ),
80        }
81    }
82}
83
84impl std::error::Error for QotSymbolParseError {}
85
86pub fn parse_qot_symbol_parts(raw: &str) -> Result<ParsedQotSymbol, QotSymbolParseError> {
87    validate_full_symbol_len(raw)?;
88    let (market_str, code) =
89        raw.split_once('.')
90            .ok_or_else(|| QotSymbolParseError::MissingSeparator {
91                raw: raw.to_string(),
92            })?;
93    if code.is_empty() {
94        return Err(QotSymbolParseError::EmptyCode {
95            raw: raw.to_string(),
96        });
97    }
98    validate_symbol_code_len(code)?;
99
100    let market_upper = market_str.to_ascii_uppercase();
101    let market = match market_upper.as_str() {
102        "HK" => 1,
103        "HK_FUTURE" => 2,
104        "US" => 11,
105        "SH" => 21,
106        "SZ" => 22,
107        "SG" => 31,
108        "JP" => 41,
109        "AU" => 51,
110        "MY" => 61,
111        "CA" => 71,
112        "FX" => 81,
113        "CRYPTO" | "CC" => 91,
114        // Ref: APIServer_Inner_API.cpp:5306-5307 GetMarketCodePrefix.
115        "EC" => 101,
116        _ => {
117            return Err(QotSymbolParseError::UnknownMarket {
118                raw: raw.to_string(),
119                market: market_upper,
120            });
121        }
122    };
123
124    Ok(ParsedQotSymbol {
125        market,
126        code: code.to_string(),
127    })
128}
129
130#[must_use]
131pub fn qot_market_from_str_alias(raw: &str) -> Option<i32> {
132    Some(match raw.trim().to_ascii_uppercase().as_str() {
133        "HK" => 1,
134        "HK_FUTURE" | "HKFUTURE" => 2,
135        "US" => 11,
136        "CN" | "SH" => 21,
137        "SZ" => 22,
138        "SG" => 31,
139        "JP" => 41,
140        "AU" => 51,
141        "MY" => 61,
142        "CA" => 71,
143        "FX" => 81,
144        "CRYPTO" | "CC" => 91,
145        "EC" | "EVENT_CONTRACT" | "EVENTCONTRACT" => 101,
146        _ => return None,
147    })
148}
149
150pub fn validate_full_symbol_len(raw: &str) -> Result<(), QotSymbolParseError> {
151    if raw.len() > MAX_SYMBOL_LEN {
152        return Err(QotSymbolParseError::SymbolTooLong { len: raw.len() });
153    }
154    Ok(())
155}
156
157pub fn validate_symbol_code_len(code: &str) -> Result<(), QotSymbolParseError> {
158    if code.len() > MAX_SYMBOL_CODE_LEN {
159        return Err(QotSymbolParseError::CodeTooLong { len: code.len() });
160    }
161    Ok(())
162}
163
164#[must_use]
165pub fn format_qot_symbol(market: i32, code: &str) -> String {
166    let prefix = qot_market_display_prefix(QotMarketId::new(market)).unwrap_or("UNKNOWN");
167    format!("{prefix}.{code}")
168}