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#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParsedQotSymbol {
22    pub market: i32,
23    pub code: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum QotSymbolParseError {
28    SymbolTooLong { len: usize },
29    MissingSeparator { raw: String },
30    EmptyCode { raw: String },
31    CodeTooLong { len: usize },
32    UnknownMarket { raw: String, market: String },
33}
34
35impl fmt::Display for QotSymbolParseError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            Self::SymbolTooLong { len } => write!(
39                f,
40                "invalid symbol: symbol length {len} exceeds max {MAX_SYMBOL_LEN}"
41            ),
42            Self::MissingSeparator { raw } => write!(
43                f,
44                "invalid symbol {raw:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)"
45            ),
46            Self::EmptyCode { raw } => {
47                write!(f, "invalid symbol {raw:?}: code part is empty")
48            }
49            Self::CodeTooLong { len } => write!(
50                f,
51                "invalid symbol code: code length {len} exceeds max {MAX_SYMBOL_CODE_LEN}"
52            ),
53            Self::UnknownMarket { raw, market } => write!(
54                f,
55                "invalid symbol {raw:?}: unknown market {market:?} (supported: {SUPPORTED_SYMBOL_MARKETS})"
56            ),
57        }
58    }
59}
60
61impl std::error::Error for QotSymbolParseError {}
62
63pub fn parse_qot_symbol_parts(raw: &str) -> Result<ParsedQotSymbol, QotSymbolParseError> {
64    validate_full_symbol_len(raw)?;
65    let (market_str, code) =
66        raw.split_once('.')
67            .ok_or_else(|| QotSymbolParseError::MissingSeparator {
68                raw: raw.to_string(),
69            })?;
70    if code.is_empty() {
71        return Err(QotSymbolParseError::EmptyCode {
72            raw: raw.to_string(),
73        });
74    }
75    validate_symbol_code_len(code)?;
76
77    let market_upper = market_str.to_ascii_uppercase();
78    let market = match market_upper.as_str() {
79        "HK" => 1,
80        "HK_FUTURE" => 2,
81        "US" => 11,
82        "SH" => 21,
83        "SZ" => 22,
84        "SG" => 31,
85        "JP" => 41,
86        "AU" => 51,
87        "MY" => 61,
88        "CA" => 71,
89        "FX" => 81,
90        "CRYPTO" | "CC" => 91,
91        // Ref: APIServer_Inner_API.cpp:5306-5307 GetMarketCodePrefix.
92        "EC" => 101,
93        _ => {
94            return Err(QotSymbolParseError::UnknownMarket {
95                raw: raw.to_string(),
96                market: market_upper,
97            });
98        }
99    };
100
101    Ok(ParsedQotSymbol {
102        market,
103        code: code.to_string(),
104    })
105}
106
107#[must_use]
108pub fn qot_market_from_str_alias(raw: &str) -> Option<i32> {
109    Some(match raw.trim().to_ascii_uppercase().as_str() {
110        "HK" => 1,
111        "HK_FUTURE" | "HKFUTURE" => 2,
112        "US" => 11,
113        "CN" | "SH" => 21,
114        "SZ" => 22,
115        "SG" => 31,
116        "JP" => 41,
117        "AU" => 51,
118        "MY" => 61,
119        "CA" => 71,
120        "FX" => 81,
121        "CRYPTO" | "CC" => 91,
122        "EC" | "EVENT_CONTRACT" | "EVENTCONTRACT" => 101,
123        _ => return None,
124    })
125}
126
127pub fn validate_full_symbol_len(raw: &str) -> Result<(), QotSymbolParseError> {
128    if raw.len() > MAX_SYMBOL_LEN {
129        return Err(QotSymbolParseError::SymbolTooLong { len: raw.len() });
130    }
131    Ok(())
132}
133
134pub fn validate_symbol_code_len(code: &str) -> Result<(), QotSymbolParseError> {
135    if code.len() > MAX_SYMBOL_CODE_LEN {
136        return Err(QotSymbolParseError::CodeTooLong { len: code.len() });
137    }
138    Ok(())
139}
140
141#[must_use]
142pub fn format_qot_symbol(market: i32, code: &str) -> String {
143    let prefix = qot_market_display_prefix(QotMarketId::new(market)).unwrap_or("UNKNOWN");
144    format!("{prefix}.{code}")
145}