From 3a53f90b14eb681da90f4e50448ed5172f2cb568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Mon, 20 Jul 2026 10:35:42 +0800 Subject: [PATCH 1/2] feat: reject non-standard symbols in all US API methods Add validate_symbol() to rust/src/utils/counter.rs and apply it to: - All 9 US fundamental methods (us_company_overview, us_valuation_overview, us_financial_overview, us_financial_statement, us_key_financial_metrics, us_analyst_consensus, us_etf_dividend_info, us_company_dividends, us_etf_files) - us_crypto_overview (quote context) - us_query_orders optional symbol filter (trade context) Bare tickers like "AXTI" now return InvalidSecuritySymbol error. Blocking wrappers inherit validation via async delegation. --- rust/src/fundamental/context.rs | 38 ++++++++++++++++++++++++--------- rust/src/quote/context.rs | 6 ++++-- rust/src/trade/context.rs | 14 ++++++------ rust/src/utils/counter.rs | 14 ++++++++++++ 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index a31c28e4f..7d671d92c 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -7,7 +7,7 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; use crate::{ Config, Result, fundamental::types::*, - utils::counter::{counter_id_to_symbol, symbol_to_counter_id}, + utils::counter::{counter_id_to_symbol, symbol_to_counter_id, validate_symbol}, }; /// Convert a Unix-seconds string to RFC 3339. @@ -1066,6 +1066,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1073,7 +1075,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/company-overview", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, DcRegion::Us, ) @@ -1089,6 +1091,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1096,7 +1100,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/valuation-overview", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, DcRegion::Us, ) @@ -1115,6 +1119,8 @@ impl FundamentalContext { symbol: impl Into, report: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1123,7 +1129,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/finn-overview", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), report: report.into(), }, DcRegion::Us, @@ -1145,6 +1151,8 @@ impl FundamentalContext { kind: impl Into, report: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1154,7 +1162,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/quote/financials/statements", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), kind: kind.into(), report: report.into(), }, @@ -1175,6 +1183,8 @@ impl FundamentalContext { symbol: impl Into, report: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1183,7 +1193,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/fin-keyfactor", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), report: report.into(), }, DcRegion::Us, @@ -1203,6 +1213,8 @@ impl FundamentalContext { symbol: impl Into, report: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1211,7 +1223,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/fin-consensus", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), report: report.into(), }, DcRegion::Us, @@ -1228,6 +1240,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1235,7 +1249,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/etf-dividend-info", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, DcRegion::Us, ) @@ -1251,6 +1265,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1258,7 +1274,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/company-dividends", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, DcRegion::Us, ) @@ -1278,6 +1294,8 @@ impl FundamentalContext { symbol: impl Into, size: Option, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -1287,7 +1305,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/stock-info/etf-files", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), size, }, DcRegion::Us, diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index b4444b6e8..168a60e4d 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2301,7 +2301,9 @@ impl QuoteContext { &self, symbol: impl Into, ) -> Result { - use crate::utils::counter::symbol_to_counter_id; + use crate::utils::counter::{symbol_to_counter_id, validate_symbol}; + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -2312,7 +2314,7 @@ impl QuoteContext { .request(Method::GET, "/v1/us/gemini/crypto-overview") .dc_restrict(DcRegion::Us) .query_params(Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }) .response::>() .send() diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 4285a5167..10513ec9e 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -867,7 +867,7 @@ impl TradeContext { pub async fn us_query_orders(&self, opts: GetUSHistoryOrders) -> Result { use std::time::{SystemTime, UNIX_EPOCH}; - use crate::utils::counter::symbol_to_counter_id; + use crate::utils::counter::{symbol_to_counter_id, validate_symbol}; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -880,12 +880,12 @@ impl TradeContext { _ => 0, }; - let counter_ids = opts - .symbol - .as_deref() - .filter(|s| !s.is_empty()) - .map(|s| vec![symbol_to_counter_id(s)]) - .unwrap_or_default(); + let counter_ids = if let Some(s) = opts.symbol.as_deref().filter(|s| !s.is_empty()) { + validate_symbol(s)?; + vec![symbol_to_counter_id(s)] + } else { + vec![] + }; let start_at = if opts.start_at == 0 { (now - 90 * 24 * 3600) as f64 diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index f6f69b46b..18569f687 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -111,6 +111,20 @@ pub fn cache_counter_ids<'a>(counter_ids: impl IntoIterator) { /// when the symbol is unknown locally — i.e. [`symbol_to_counter_id`] would /// fall back to the default `ST/` prefix, which may be wrong for newly /// listed ETFs / indexes / warrants. +/// Returns `Ok(())` if `symbol` is in the required `CODE.MARKET` format +/// (e.g. `"AAPL.US"`, `"BTCUSD.BKKT"`), or an `Err` describing the problem. +/// +/// Bare tickers such as `"AXTI"` are rejected. +pub fn validate_symbol(symbol: &str) -> crate::Result<()> { + let dot = symbol.rfind('.'); + match dot { + Some(i) if i > 0 && i < symbol.len() - 1 => Ok(()), + _ => Err(crate::Error::InvalidSecuritySymbol { + symbol: format!("{symbol} (expected CODE.MARKET format, e.g. \"AAPL.US\")"), + }), + } +} + pub fn lookup_counter_id(symbol: &str) -> Option { let (code, market) = symbol.rsplit_once('.')?; let market = market.to_uppercase(); From 48146f35d5c95411619f602d02aca0a8e8af8def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Mon, 20 Jul 2026 11:11:05 +0800 Subject: [PATCH 2/2] feat: reject non-standard symbols in all Rust API methods Add validate_and_convert() to counter.rs. Apply to: - All 28 non-US fundamental methods - 8 quote methods (depth, brokers, trades, intraday, candlesticks, etc.) - 2 trade methods (margin_ratio, us_query_orders) --- rust/src/fundamental/context.rs | 112 ++++++++++++++++++++++++-------- rust/src/quote/context.rs | 19 +++++- rust/src/trade/context.rs | 2 + rust/src/utils/counter.rs | 9 +++ 4 files changed, 112 insertions(+), 30 deletions(-) diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index 7d671d92c..93b3ca46a 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -111,6 +111,8 @@ impl FundamentalContext { kind: FinancialReportKind, period: Option, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; let kind_str = match kind { FinancialReportKind::IncomeStatement => "IS", FinancialReportKind::BalanceSheet => "BS", @@ -136,7 +138,7 @@ impl FundamentalContext { self.get( "/v1/quote/financial-reports", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), kind: kind_str, report: period_str, }, @@ -151,11 +153,13 @@ impl FundamentalContext { /// Path: `GET /v1/quote/institution-rating-latest` + /// `GET /v1/quote/institution-ratings` pub async fn institution_rating(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, } - let cid = symbol_to_counter_id(&symbol.into()); + let cid = symbol_to_counter_id(&symbol); let q = Query { counter_id: cid }; let (latest, summary) = tokio::join!( self.get::( @@ -184,6 +188,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -191,7 +197,7 @@ impl FundamentalContext { self.get( "/v1/quote/institution-ratings/detail", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -203,6 +209,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/dividends` pub async fn dividend(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -210,7 +218,7 @@ impl FundamentalContext { self.get( "/v1/quote/dividends", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -220,6 +228,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/dividends/details` pub async fn dividend_detail(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -227,7 +237,7 @@ impl FundamentalContext { self.get( "/v1/quote/dividends/details", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -239,6 +249,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/forecast-eps` pub async fn forecast_eps(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -246,7 +258,7 @@ impl FundamentalContext { self.get( "/v1/quote/forecast-eps", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -258,6 +270,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/financial-consensus-detail` pub async fn consensus(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -265,7 +279,7 @@ impl FundamentalContext { self.get( "/v1/quote/financial-consensus-detail", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -277,6 +291,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/valuation` pub async fn valuation(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -286,7 +302,7 @@ impl FundamentalContext { self.get( "/v1/quote/valuation", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), indicator: "pe", range: "1", }, @@ -301,6 +317,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -308,7 +326,7 @@ impl FundamentalContext { self.get( "/v1/quote/valuation/detail", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -323,6 +341,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -330,7 +350,7 @@ impl FundamentalContext { self.get( "/v1/quote/industry-valuation-comparison", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -343,6 +363,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -350,7 +372,7 @@ impl FundamentalContext { self.get( "/v1/quote/industry-valuation-distribution", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -362,6 +384,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/comp-overview` pub async fn company(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -369,7 +393,7 @@ impl FundamentalContext { self.get( "/v1/quote/comp-overview", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -381,6 +405,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/company-professionals` pub async fn executive(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_ids: String, @@ -388,7 +414,7 @@ impl FundamentalContext { self.get( "/v1/quote/company-professionals", Query { - counter_ids: symbol_to_counter_id(&symbol.into()), + counter_ids: symbol_to_counter_id(&symbol), }, ) .await @@ -400,6 +426,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/shareholders` pub async fn shareholder(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -407,7 +435,7 @@ impl FundamentalContext { self.get( "/v1/quote/shareholders", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -419,6 +447,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/fund-holders` pub async fn fund_holder(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -426,7 +456,7 @@ impl FundamentalContext { self.get( "/v1/quote/fund-holders", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -438,6 +468,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/company-act` pub async fn corp_action(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -447,7 +479,7 @@ impl FundamentalContext { self.get( "/v1/quote/company-act", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), req_type: "1", version: "3", }, @@ -461,6 +493,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/invest-relations` pub async fn invest_relation(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -469,7 +503,7 @@ impl FundamentalContext { self.get( "/v1/quote/invest-relations", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), count: "0", }, ) @@ -482,6 +516,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/operatings` pub async fn operating(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -489,7 +525,7 @@ impl FundamentalContext { self.get_dc( "/v1/quote/operatings", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, DcRegion::Ap, ) @@ -502,6 +538,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/buy-backs` pub async fn buyback(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -509,7 +547,7 @@ impl FundamentalContext { self.get( "/v1/quote/buy-backs", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -521,6 +559,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/ratings` pub async fn ratings(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -528,7 +568,7 @@ impl FundamentalContext { self.get( "/v1/quote/ratings", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -540,6 +580,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/quote/fundamentals/business-segments` pub async fn business_segments(&self, symbol: impl Into) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -547,7 +589,7 @@ impl FundamentalContext { self.get( "/v1/quote/fundamentals/business-segments", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -562,6 +604,8 @@ impl FundamentalContext { report: Option<&'static str>, cate: Option, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -573,7 +617,7 @@ impl FundamentalContext { self.get( "/v1/quote/fundamentals/business-segments/history", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), report, cate, }, @@ -590,6 +634,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -598,7 +644,7 @@ impl FundamentalContext { .get( "/v1/quote/shareholders/top", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await?; @@ -614,6 +660,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -621,7 +669,7 @@ impl FundamentalContext { self.get( "/v1/quote/ratings/institutional", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await @@ -637,6 +685,8 @@ impl FundamentalContext { symbol: impl Into, object_id: i64, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -646,7 +696,7 @@ impl FundamentalContext { .get( "/v1/quote/shareholders/holding", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), object_id: object_id.to_string(), }, ) @@ -734,6 +784,8 @@ impl FundamentalContext { fiscal_year: Option, fiscal_period: Option<&'static str>, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -747,7 +799,7 @@ impl FundamentalContext { self.get( "/v1/quote/financials/earnings-snapshot", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), report, fiscal_year, fiscal_period, @@ -767,6 +819,8 @@ impl FundamentalContext { currency: impl Into, comparison_symbols: Option>, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -782,7 +836,7 @@ impl FundamentalContext { .get( "/v1/quote/compare/valuation", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), currency: currency.into(), comparison_counter_ids, }, @@ -838,6 +892,8 @@ impl FundamentalContext { &self, symbol: impl Into, ) -> Result { + let symbol: String = symbol.into(); + validate_symbol(&symbol)?; #[derive(Serialize)] struct Query { counter_id: String, @@ -845,7 +901,7 @@ impl FundamentalContext { self.get( "/v1/quote/etf-asset-allocation", Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol), }, ) .await diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 168a60e4d..43b3c3f84 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -622,6 +622,8 @@ impl QuoteContext { /// # }); /// ``` pub async fn depth(&self, symbol: impl Into) -> Result { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; let resp: quote::SecurityDepthResponse = self .request( cmd_code::GET_SECURITY_DEPTH, @@ -668,6 +670,8 @@ impl QuoteContext { /// # }); /// ``` pub async fn brokers(&self, symbol: impl Into) -> Result { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; // Broker queue is served only by the AP data center; short-circuit a // non-AP session with the same unified error the HTTP path returns. let current = self.0.http_cli.dc_region().await; @@ -759,6 +763,8 @@ impl QuoteContext { /// # }); /// ``` pub async fn trades(&self, symbol: impl Into, count: usize) -> Result> { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; let resp: quote::SecurityTradeResponse = self .request( cmd_code::GET_SECURITY_TRADES, @@ -1310,6 +1316,8 @@ impl QuoteContext { /// # Ok::<_, Box>(()) /// # }); pub async fn capital_flow(&self, symbol: impl Into) -> Result> { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; self.request::<_, quote::CapitalFlowIntradayResponse>( cmd_code::GET_CAPITAL_FLOW_INTRADAY, quote::CapitalFlowIntradayRequest { @@ -1621,6 +1629,8 @@ impl QuoteContext { /// Get filings list pub async fn filings(&self, symbol: impl Into) -> Result> { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; #[derive(Debug, Serialize)] struct Request { symbol: String, @@ -1823,6 +1833,8 @@ impl QuoteContext { /// # }); /// ``` pub async fn realtime_depth(&self, symbol: impl Into) -> Result { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; let (reply_tx, reply_rx) = oneshot::channel(); self.0 .command_tx @@ -1917,6 +1929,8 @@ impl QuoteContext { /// # }); /// ``` pub async fn realtime_brokers(&self, symbol: impl Into) -> Result { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; let (reply_tx, reply_rx) = oneshot::channel(); self.0 .command_tx @@ -2066,7 +2080,8 @@ impl QuoteContext { /// /// Path: `GET /v1/quote/option-volume-stats` pub async fn option_volume(&self, symbol: impl Into) -> Result { - use crate::utils::counter::symbol_to_counter_id; + use crate::utils::counter::{symbol_to_counter_id, validate_and_convert}; + let symbol = validate_and_convert(symbol)?; #[derive(serde::Serialize)] struct Query { underlying_counter_id: String, @@ -2076,7 +2091,7 @@ impl QuoteContext { .http_cli .request(Method::GET, "/v1/quote/option-volume-stats") .query_params(Query { - underlying_counter_id: symbol_to_counter_id(&symbol.into()), + underlying_counter_id: symbol_to_counter_id(&symbol), }) .response::>() .send() diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 10513ec9e..7984d8c48 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -738,6 +738,8 @@ impl TradeContext { /// # }); /// ``` pub async fn margin_ratio(&self, symbol: impl Into) -> Result { + use crate::utils::counter::validate_and_convert; + let symbol = validate_and_convert(symbol)?; #[derive(Debug, Serialize)] struct Request { symbol: String, diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index 18569f687..4faaa48e2 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -115,6 +115,15 @@ pub fn cache_counter_ids<'a>(counter_ids: impl IntoIterator) { /// (e.g. `"AAPL.US"`, `"BTCUSD.BKKT"`), or an `Err` describing the problem. /// /// Bare tickers such as `"AXTI"` are rejected. +/// Validates `symbol`, converts it to a `String`, and returns it. +/// Convenience wrapper that validates and consumes `impl Into` in one +/// step. +pub fn validate_and_convert(symbol: impl Into) -> crate::Result { + let s = symbol.into(); + validate_symbol(&s)?; + Ok(s) +} + pub fn validate_symbol(symbol: &str) -> crate::Result<()> { let dot = symbol.rfind('.'); match dot {