Skip to main content

futu_qot/
symbol.rs

1use anyhow::{Result, anyhow, bail};
2
3use crate::types::{QotMarket, Security};
4
5const SUPPORTED_SYMBOL_MARKETS: &str =
6    "HK, HK_FUTURE, US, SH, SZ, SG, JP, AU, MY, CA, FX, CRYPTO, CC";
7/// Defensive user-input cap for `MARKET.CODE` shorthand strings.
8///
9/// C++ stores market-state and trade code buffers in fixed char arrays
10/// (`charArr64` / `szCode[32]` in the audited paths). Keep Rust's public
11/// shorthand cap comfortably above all observed legitimate derivatives, while
12/// rejecting pathological payloads before REST/MCP/CLI normalization allocates
13/// or forwards them.
14pub const MAX_SYMBOL_LEN: usize = 160;
15pub const MAX_SYMBOL_CODE_LEN: usize = 128;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ParsedSymbol {
19    pub market: QotMarket,
20    pub code: String,
21}
22
23impl ParsedSymbol {
24    pub fn market_i32(&self) -> i32 {
25        self.market as i32
26    }
27
28    pub fn into_security(self) -> Security {
29        Security::new(self.market, self.code)
30    }
31}
32
33pub fn parse_symbol_parts(s: &str) -> Result<ParsedSymbol> {
34    validate_full_symbol_len(s)?;
35    let (market_str, code) = s.split_once('.').ok_or_else(|| {
36        anyhow!("invalid symbol {s:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)")
37    })?;
38    if code.is_empty() {
39        bail!("invalid symbol {s:?}: code part is empty");
40    }
41    validate_symbol_code_len(code)?;
42
43    let market = match market_str.to_ascii_uppercase().as_str() {
44        "HK" => QotMarket::HkSecurity,
45        "HK_FUTURE" => QotMarket::HkFuture,
46        "US" => QotMarket::UsSecurity,
47        "SH" => QotMarket::CnshSecurity,
48        "SZ" => QotMarket::CnszSecurity,
49        "SG" => QotMarket::SgSecurity,
50        "JP" => QotMarket::JpSecurity,
51        "AU" => QotMarket::AuSecurity,
52        "MY" => QotMarket::MySecurity,
53        "CA" => QotMarket::CaSecurity,
54        "FX" => QotMarket::FxSecurity,
55        "CRYPTO" | "CC" => QotMarket::Crypto,
56        other => bail!(
57            "invalid symbol {s:?}: unknown market {other:?} (supported: {SUPPORTED_SYMBOL_MARKETS})"
58        ),
59    };
60
61    Ok(ParsedSymbol {
62        market,
63        code: code.to_string(),
64    })
65}
66
67pub fn validate_full_symbol_len(s: &str) -> Result<()> {
68    if s.len() > MAX_SYMBOL_LEN {
69        bail!(
70            "invalid symbol {s:?}: symbol length {} exceeds max {MAX_SYMBOL_LEN}",
71            s.len()
72        );
73    }
74    Ok(())
75}
76
77pub fn validate_symbol_code_len(code: &str) -> Result<()> {
78    if code.len() > MAX_SYMBOL_CODE_LEN {
79        bail!(
80            "invalid symbol code: code length {} exceeds max {MAX_SYMBOL_CODE_LEN}",
81            code.len()
82        );
83    }
84    Ok(())
85}
86
87pub fn parse_symbol(s: &str) -> Result<Security> {
88    Ok(parse_symbol_parts(s)?.into_security())
89}
90
91pub fn format_symbol(sec: &Security) -> String {
92    let market = match sec.market {
93        QotMarket::HkSecurity => "HK",
94        QotMarket::HkFuture => "HK_FUTURE",
95        QotMarket::UsSecurity => "US",
96        QotMarket::CnshSecurity => "SH",
97        QotMarket::CnszSecurity => "SZ",
98        QotMarket::SgSecurity => "SG",
99        QotMarket::JpSecurity => "JP",
100        QotMarket::AuSecurity => "AU",
101        QotMarket::MySecurity => "MY",
102        QotMarket::CaSecurity => "CA",
103        QotMarket::FxSecurity => "FX",
104        QotMarket::Crypto => "CC",
105        QotMarket::Unknown => "UNKNOWN",
106    };
107    format!("{market}.{}", sec.code)
108}
109
110#[cfg(test)]
111mod tests;