use std::{collections::HashSet, sync::OnceLock};
static US_ETF_SET: OnceLock<HashSet<&'static str>> = OnceLock::new();
fn us_etf_set() -> &'static HashSet<&'static str> {
US_ETF_SET.get_or_init(|| {
include_str!("US-ETF.csv")
.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.collect()
})
}
pub(crate) fn symbol_to_counter_id(symbol: &str) -> String {
if let Some((code, market)) = symbol.rsplit_once('.') {
let market = market.to_uppercase();
let etf_candidate = format!("ETF/{market}/{code}");
if us_etf_set().contains(etf_candidate.as_str()) {
etf_candidate
} else {
format!("ST/{market}/{code}")
}
} else {
symbol.to_string()
}
}
pub(crate) fn index_symbol_to_counter_id(symbol: &str) -> String {
if let Some((code, market)) = symbol.rsplit_once('.') {
format!("IX/{}/{code}", market.to_uppercase())
} else {
symbol.to_string()
}
}
pub(crate) fn counter_id_to_symbol(counter_id: &str) -> String {
let parts: Vec<&str> = counter_id.splitn(3, '/').collect();
if parts.len() == 3 {
format!("{}.{}", parts[2], parts[1])
} else {
counter_id.to_string()
}
}
pub(crate) fn deserialize_counter_id_as_symbol<'de, D>(d: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let counter_id = String::deserialize(d)?;
Ok(counter_id_to_symbol(&counter_id))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stock_us() {
assert_eq!(symbol_to_counter_id("TSLA.US"), "ST/US/TSLA");
}
#[test]
fn stock_hk() {
assert_eq!(symbol_to_counter_id("700.HK"), "ST/HK/700");
}
#[test]
fn etf_us() {
assert_eq!(symbol_to_counter_id("SPY.US"), "ETF/US/SPY");
}
#[test]
fn index() {
assert_eq!(index_symbol_to_counter_id("HSI.HK"), "IX/HK/HSI");
}
#[test]
fn roundtrip() {
let cid = symbol_to_counter_id("TSLA.US");
assert_eq!(counter_id_to_symbol(&cid), "TSLA.US");
}
}