use serde::Serialize;
use serde_json::Value;
use crate::client::api_request::ApiRequest;
use crate::client::decode::decode_value;
use crate::client::http_client::HttpClient;
use crate::error::TigerError;
use crate::model::quote::{
Brief, CapitalDistribution, CapitalFlow, CorporateAction, CorporateActionRequest, Depth,
ExchangeRate, FinancialCurrency, FinancialDailyItem, FinancialDailyRequest,
FinancialReportItem, FinancialReportRequest, FundContractInfo, FundHistoryQuote, FundQuote,
FutureContractInfo, FutureDepth, FutureExchange, FutureKline, FutureKlineRequest,
FutureMainContractHistory, FutureQuote, FutureTradingTime, FutureTradeTickItem,
IndustryItem, IndustryStock, Kline, KlineItem, KlineQuota, MarketScannerRequest,
MarketScannerTags, MarketState, OptionAnalysis, OptionBrief, OptionChain, OptionExpiration,
OptionKline, OptionSymbol, QuoteOvernight, QuotePermission, ScannerResult, ShortInterest,
StockBroker, StockDetail, StockIndustry, SymbolName, Timeline, TradeTick, TradeRankItem,
TradingCalendarItem, WarrantBrief, WarrantFilterResult, FutureKlineItem,
};
use crate::model::quote_requests::{
AllFutureContractsRequest, BarsRequest, BarsByPageRequest, BriefRequest, DepthQuoteRequest,
FinancialCurrencyRequest, FinancialExchangeRateRequest, FutureBarsRequest,
FutureBarsByPageRequest, FutureBriefRequest, FutureContinuousContractsRequest,
FutureContractSingleRequest, FutureDepthRequest, FutureHistoryMainContractRequest,
FutureTradingTimesRequest, FutureTradeTicksRequest, FundContractsRequest, FundHistoryQuoteRequest,
FundQuoteRequest, FundSymbolsRequest, IndustryListRequest, IndustryStocksRequest,
KlineQuotaRequest, MarketScannerTagsRequest, OptionAnalysisRequest, OptionBarsRequest,
OptionDepthRequest, OptionSymbolsRequest, OptionTimelineRequest, OptionTradeTicksRequest,
QuoteOvernightRequest, QuotePermissionRequest, ShortInterestRequest, StockBrokerRequest,
StockDelayBriefsRequest, StockDetailsRequest, StockFundamentalRequest, StockIndustryRequest,
SymbolsRequest, TimelineHistoryRequest, TradeMetasRequest, TradeRankRequest, TradeTickRequest,
TradingCalendarRequest, WarrantBriefsRequest, WarrantFilterRequest,
};
const VERSION_V1: &str = "1.0";
const VERSION_V2: &str = "2.0";
const VERSION_V3: &str = "3.0";
pub struct QuoteClient<'a> {
http_client: &'a HttpClient,
}
impl<'a> QuoteClient<'a> {
pub fn new(http_client: &'a HttpClient) -> Self {
Self { http_client }
}
async fn call_into<T, P>(&self, method: &str, params: P) -> Result<T, TigerError>
where
T: serde::de::DeserializeOwned + Default,
P: Serialize,
{
self.call_into_versioned(method, params, None).await
}
async fn call_into_versioned<T, P>(
&self,
method: &str,
params: P,
version: Option<&str>,
) -> Result<T, TigerError>
where
T: serde::de::DeserializeOwned + Default,
P: Serialize,
{
let biz = serde_json::to_string(¶ms)
.map_err(|e| TigerError::Config(format!("serialize biz params failed: {}", e)))?;
let req = match version {
Some(v) => ApiRequest::with_version(method, biz, v),
None => ApiRequest::new(method, biz),
};
let resp = self.http_client.execute_request(&req).await?;
unmarshal_data(resp.data)
}
async fn call_into_items<T, P>(&self, method: &str, params: P) -> Result<Vec<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let wrap: serde_json::Value = self.call_into(method, params).await?;
match wrap.get("items") {
None | Some(Value::Null) => Ok(vec![]),
Some(items) => serde_json::from_value(items.clone())
.map_err(|e| TigerError::Config(format!("decode items failed: {}", e))),
}
}
async fn call_into_list_or_object<T, P>(
&self,
method: &str,
params: P,
) -> Result<Vec<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let biz = serde_json::to_string(¶ms)
.map_err(|e| TigerError::Config(format!("serialize biz params failed: {}", e)))?;
let req = ApiRequest::new(method, biz);
let resp = self.http_client.execute_request(&req).await?;
let raw = match resp.data {
None | Some(Value::Null) => return Ok(vec![]),
Some(v) => v,
};
if let Ok(list) = serde_json::from_value::<Vec<T>>(raw.clone()) {
return Ok(list);
}
let inner = if let Value::String(s) = &raw {
serde_json::from_str::<Value>(s)
.map_err(|e| TigerError::Config(format!("decode double-encoded data: {}", e)))?
} else {
raw
};
let obj: T = serde_json::from_value(inner)
.map_err(|e| TigerError::Config(format!("decode single object failed: {}", e)))?;
Ok(vec![obj])
}
pub async fn get_market_state(&self, market: &str) -> Result<Vec<MarketState>, TigerError> {
self.call_into("market_state", serde_json::json!({ "market": market })).await
}
pub async fn get_brief(&self, req: BriefRequest) -> Result<Vec<Brief>, TigerError> {
self.call_into("quote_real_time", req).await
}
pub async fn get_kline(&self, symbol: &str, period: &str) -> Result<Vec<Kline>, TigerError> {
self.call_into("kline", serde_json::json!({ "symbols": [symbol], "period": period })).await
}
pub async fn get_timeline(&self, symbols: &[&str]) -> Result<Vec<Timeline>, TigerError> {
self.call_into_versioned(
"timeline",
serde_json::json!({ "symbols": symbols }),
Some(VERSION_V3),
)
.await
}
pub async fn get_trade_tick(&self, req: TradeTickRequest) -> Result<Vec<TradeTick>, TigerError> {
self.call_into("trade_tick", req).await
}
pub async fn get_quote_depth(&self, req: DepthQuoteRequest) -> Result<Vec<Depth>, TigerError> {
self.call_into("quote_depth", req).await
}
pub async fn get_symbols(&self, req: SymbolsRequest) -> Result<Vec<String>, TigerError> {
self.call_into("all_symbols", req).await
}
pub async fn get_symbol_names(&self, req: SymbolsRequest) -> Result<Vec<SymbolName>, TigerError> {
self.call_into("all_symbol_names", req).await
}
pub async fn get_trade_metas(&self, req: TradeMetasRequest) -> Result<Vec<crate::model::quote::TradeMeta>, TigerError> {
self.call_into("quote_stock_trade", req).await
}
pub async fn get_stock_details(&self, req: StockDetailsRequest) -> Result<Vec<StockDetail>, TigerError> {
self.call_into_items("stock_detail", req).await
}
pub async fn get_stock_delay_briefs(&self, req: StockDelayBriefsRequest) -> Result<Vec<Brief>, TigerError> {
self.call_into("quote_delay", req).await
}
pub async fn get_bars(&self, req: BarsRequest) -> Result<Vec<Kline>, TigerError> {
self.call_into("kline", req).await
}
pub async fn get_bars_by_page(&self, req: BarsByPageRequest) -> Result<Vec<KlineItem>, TigerError> {
let page_size = req.page_size.unwrap_or(200);
let total_size = req.total_size.unwrap_or(1000);
let mut acc: Vec<KlineItem> = Vec::new();
let mut end_time = req.end_time;
let begin_time = req.begin_time;
while (acc.len() as i32) < total_size {
let sub = BarsRequest {
symbols: req.symbol.clone().map(|s| vec![s]),
period: req.period.clone(),
right: req.right.clone(),
begin_time,
end_time,
limit: Some(page_size),
trade_session: req.trade_session.clone(),
lang: req.lang.clone(),
..Default::default()
};
let page_out: Vec<Kline> = self.call_into("kline", sub).await?;
if page_out.is_empty() || page_out[0].items.is_empty() {
break;
}
let items = page_out[0].items.clone();
let len = items.len() as i32;
acc.extend(items.clone());
if len < page_size {
break;
}
let oldest = items.iter().map(|i| i.time).min().unwrap_or(0);
end_time = Some(oldest - 1);
}
Ok(acc)
}
pub async fn get_timeline_history(&self, req: TimelineHistoryRequest) -> Result<Vec<Timeline>, TigerError> {
self.call_into("history_timeline", req).await
}
pub async fn get_trade_rank(&self, req: TradeRankRequest) -> Result<Vec<TradeRankItem>, TigerError> {
self.call_into("trade_rank", req).await
}
pub async fn get_short_interest(&self, req: ShortInterestRequest) -> Result<Vec<ShortInterest>, TigerError> {
self.call_into("quote_shortable_stocks", req).await
}
pub async fn get_stock_broker(&self, req: StockBrokerRequest) -> Result<Option<StockBroker>, TigerError> {
self.call_optional("stock_broker", req).await
}
pub async fn get_stock_fundamental(
&self,
req: StockFundamentalRequest,
) -> Result<std::collections::BTreeMap<String, serde_json::Value>, TigerError> {
self.call_into("stock_fundamental", req).await
}
pub async fn get_stock_industry(&self, req: StockIndustryRequest) -> Result<Vec<StockIndustry>, TigerError> {
self.call_into("stock_industry", req).await
}
pub async fn get_quote_permission(&self, req: QuotePermissionRequest) -> Result<Vec<QuotePermission>, TigerError> {
self.call_into("get_quote_permission", req).await
}
pub async fn get_kline_quota(&self, req: KlineQuotaRequest) -> Result<Vec<KlineQuota>, TigerError> {
self.call_into("kline_quota", req).await
}
pub async fn get_option_expiration(&self, symbol: &str) -> Result<Vec<OptionExpiration>, TigerError> {
self.call_into(
"option_expiration",
serde_json::json!({ "symbols": [symbol] }),
)
.await
}
pub async fn get_option_chain(
&self,
symbol: &str,
expiry: &str,
) -> Result<Vec<OptionChain>, TigerError> {
let expiry_ts = parse_expiry_to_ms(expiry)?;
let params = serde_json::json!({
"option_basic": [{ "symbol": symbol, "expiry": expiry_ts }]
});
self.call_into_versioned("option_chain", params, Some(VERSION_V3)).await
}
pub async fn get_option_brief(
&self,
identifiers: &[&str],
) -> Result<Vec<OptionBrief>, TigerError> {
let mut option_basics: Vec<serde_json::Value> = Vec::with_capacity(identifiers.len());
for id in identifiers {
let contract = parse_option_identifier(id)?;
option_basics.push(serde_json::json!({
"symbol": contract.symbol,
"expiry": contract.expiry,
"right": contract.right,
"strike": contract.strike,
}));
}
let params = serde_json::json!({ "option_basic": option_basics });
self.call_into_versioned("option_brief", params, Some(VERSION_V2)).await
}
pub async fn get_option_kline(
&self,
identifier: &str,
period: &str,
) -> Result<Vec<OptionKline>, TigerError> {
let contract = parse_option_identifier(identifier)?;
let params = serde_json::json!({
"option_query": [{
"symbol": contract.symbol,
"expiry": contract.expiry,
"right": contract.right,
"strike": contract.strike,
"period": period,
}]
});
self.call_into_versioned("option_kline", params, Some(VERSION_V2)).await
}
pub async fn get_option_bars(&self, req: OptionBarsRequest) -> Result<Vec<Kline>, TigerError> {
self.call_into_versioned("option_kline", req, Some(VERSION_V2)).await
}
pub async fn get_option_trade_ticks(&self, req: OptionTradeTicksRequest) -> Result<Vec<TradeTick>, TigerError> {
self.call_into("option_trade_tick", req).await
}
pub async fn get_option_timeline(&self, req: OptionTimelineRequest) -> Result<Vec<Timeline>, TigerError> {
self.call_into("option_timeline", req).await
}
pub async fn get_option_depth(&self, req: OptionDepthRequest) -> Result<Vec<Depth>, TigerError> {
self.call_into("option_depth", req).await
}
pub async fn get_option_symbols(&self, req: OptionSymbolsRequest) -> Result<Vec<OptionSymbol>, TigerError> {
self.call_into("option_symbol", req).await
}
pub async fn get_option_analysis(&self, req: OptionAnalysisRequest) -> Result<Vec<OptionAnalysis>, TigerError> {
self.call_into("option_analysis", req).await
}
pub async fn get_future_exchange(&self) -> Result<Vec<FutureExchange>, TigerError> {
self.call_into("future_exchange", serde_json::json!({ "sec_type": "FUT" })).await
}
pub async fn get_future_contracts(
&self,
exchange_code: &str,
) -> Result<Vec<FutureContractInfo>, TigerError> {
self.call_into(
"future_contract_by_exchange_code",
serde_json::json!({ "exchange_code": exchange_code }),
)
.await
}
pub async fn get_future_real_time_quote(&self, req: FutureBriefRequest) -> Result<Vec<FutureQuote>, TigerError> {
self.call_into("future_real_time_quote", req).await
}
pub async fn get_future_kline(
&self,
mut req: FutureKlineRequest,
) -> Result<Vec<FutureKline>, TigerError> {
if req.begin_time == 0 {
req.begin_time = -1;
}
if req.end_time == 0 {
req.end_time = -1;
}
self.call_into("future_kline", req).await
}
pub async fn get_future_contract(&self, req: FutureContractSingleRequest) -> Result<Vec<FutureContractInfo>, TigerError> {
self.call_into_list_or_object("future_contract_by_contract_code", req).await
}
pub async fn get_all_future_contracts(&self, req: AllFutureContractsRequest) -> Result<Vec<FutureContractInfo>, TigerError> {
self.call_into("future_contracts", req).await
}
pub async fn get_current_future_contract(&self, req: FutureContractSingleRequest) -> Result<Option<FutureContractInfo>, TigerError> {
self.call_optional("future_current_contract", req).await
}
pub async fn get_future_continuous_contracts(&self, req: FutureContinuousContractsRequest) -> Result<Vec<FutureContractInfo>, TigerError> {
self.call_into_list_or_object("future_continuous_contracts", req).await
}
pub async fn get_future_history_main_contract(&self, req: FutureHistoryMainContractRequest) -> Result<Vec<FutureMainContractHistory>, TigerError> {
self.call_into("future_main_contract", req).await
}
pub async fn get_future_bars(&self, mut req: FutureBarsRequest) -> Result<Vec<FutureKline>, TigerError> {
if req.begin_time == Some(0) {
req.begin_time = Some(-1);
}
if req.end_time == Some(0) {
req.end_time = Some(-1);
}
self.call_into("future_kline", req).await
}
pub async fn get_future_bars_by_page(&self, req: FutureBarsByPageRequest) -> Result<Vec<FutureKlineItem>, TigerError> {
let page_size = req.page_size.unwrap_or(200);
let total_size = req.total_size.unwrap_or(1000);
let mut acc: Vec<FutureKlineItem> = Vec::new();
let mut end_time = if req.end_time == Some(0) || req.end_time.is_none() {
Some(-1i64)
} else {
req.end_time
};
let begin_time = if req.begin_time == Some(0) || req.begin_time.is_none() {
Some(-1i64)
} else {
req.begin_time
};
while (acc.len() as i32) < total_size {
let sub = FutureBarsRequest {
contract_code: req.contract_code.clone(),
period: req.period.clone(),
begin_time,
end_time,
limit: Some(page_size),
lang: req.lang.clone(),
..Default::default()
};
let page_out: Vec<FutureKline> = self.call_into("future_kline", sub).await?;
if page_out.is_empty() || page_out[0].items.is_empty() {
break;
}
let items = page_out[0].items.clone();
let len = items.len() as i32;
acc.extend(items.clone());
if len < page_size {
break;
}
let oldest = items.iter().map(|i| i.time).min().unwrap_or(0);
end_time = Some(oldest - 1);
}
Ok(acc)
}
pub async fn get_future_trade_ticks(&self, req: FutureTradeTicksRequest) -> Result<Vec<FutureTradeTickItem>, TigerError> {
let mut req = req;
if req.end_index.is_none() {
req.end_index = Some(30);
}
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct FutureTickWrap {
#[serde(default)]
contract_code: String,
#[serde(default)]
items: Vec<FutureTradeTickItem>,
}
let wrap: FutureTickWrap = self.call_into_versioned("future_tick", req, Some(VERSION_V3)).await?;
let mut items = wrap.items;
for item in &mut items {
if item.contract_code.is_empty() {
item.contract_code = wrap.contract_code.clone();
}
}
Ok(items)
}
pub async fn get_future_depth(&self, req: FutureDepthRequest) -> Result<Vec<FutureDepth>, TigerError> {
self.call_into("future_depth", req).await
}
pub async fn get_future_trading_times(&self, req: FutureTradingTimesRequest) -> Result<Option<FutureTradingTime>, TigerError> {
self.call_optional("future_trading_date", req).await
}
pub async fn get_fund_symbols(&self, req: FundSymbolsRequest) -> Result<Vec<String>, TigerError> {
self.call_into("fund_all_symbols", req).await
}
pub async fn get_fund_contracts(&self, req: FundContractsRequest) -> Result<Vec<FundContractInfo>, TigerError> {
self.call_into("fund_contracts", req).await
}
pub async fn get_fund_quote(&self, req: FundQuoteRequest) -> Result<Vec<FundQuote>, TigerError> {
self.call_into("fund_quote", req).await
}
pub async fn get_fund_history_quote(&self, req: FundHistoryQuoteRequest) -> Result<Vec<FundHistoryQuote>, TigerError> {
self.call_into("fund_history_quote", req).await
}
pub async fn get_warrant_briefs(&self, req: WarrantBriefsRequest) -> Result<Vec<WarrantBrief>, TigerError> {
self.call_into("warrant_briefs", req).await
}
pub async fn get_warrant_filter(&self, req: WarrantFilterRequest) -> Result<Option<WarrantFilterResult>, TigerError> {
self.call_optional("warrant_filter", req).await
}
pub async fn get_industry_list(&self, req: IndustryListRequest) -> Result<Vec<IndustryItem>, TigerError> {
self.call_into("industry_list", req).await
}
pub async fn get_industry_stocks(&self, req: IndustryStocksRequest) -> Result<Vec<IndustryStock>, TigerError> {
self.call_into("industry_stock_list", req).await
}
pub async fn get_corporate_action(
&self,
req: CorporateActionRequest,
) -> Result<Vec<CorporateAction>, TigerError> {
let grouped: std::collections::BTreeMap<String, Vec<CorporateAction>> =
self.call_into("corporate_action", req).await?;
let mut out = Vec::new();
for (_, mut list) in grouped {
out.append(&mut list);
}
Ok(out)
}
pub async fn get_corporate_split(
&self,
mut req: CorporateActionRequest,
) -> Result<Vec<CorporateAction>, TigerError> {
req.action_type = "split".into();
self.get_corporate_action(req).await
}
pub async fn get_corporate_dividend(
&self,
mut req: CorporateActionRequest,
) -> Result<Vec<CorporateAction>, TigerError> {
req.action_type = "dividend".into();
self.get_corporate_action(req).await
}
pub async fn get_corporate_earnings_calendar(
&self,
mut req: CorporateActionRequest,
) -> Result<Vec<CorporateAction>, TigerError> {
req.action_type = "earning".into();
self.get_corporate_action(req).await
}
pub async fn get_financial_daily(
&self,
req: FinancialDailyRequest,
) -> Result<Vec<FinancialDailyItem>, TigerError> {
self.call_into("financial_daily", req).await
}
pub async fn get_financial_report(
&self,
req: FinancialReportRequest,
) -> Result<Vec<FinancialReportItem>, TigerError> {
self.call_into("financial_report", req).await
}
pub async fn get_financial_currency(&self, req: FinancialCurrencyRequest) -> Result<Vec<FinancialCurrency>, TigerError> {
self.call_into("financial_currency", req).await
}
pub async fn get_financial_exchange_rate(&self, req: FinancialExchangeRateRequest) -> Result<Vec<ExchangeRate>, TigerError> {
self.call_into("financial_exchange_rate", req).await
}
pub async fn get_trading_calendar(&self, req: TradingCalendarRequest) -> Result<Vec<TradingCalendarItem>, TigerError> {
self.call_into("trading_calendar", req).await
}
pub async fn get_capital_flow(
&self,
symbol: &str,
market: &str,
period: &str,
) -> Result<Option<CapitalFlow>, TigerError> {
self.call_optional(
"capital_flow",
serde_json::json!({ "symbol": symbol, "market": market, "period": period }),
)
.await
}
pub async fn get_capital_distribution(
&self,
symbol: &str,
market: &str,
) -> Result<Option<CapitalDistribution>, TigerError> {
self.call_optional(
"capital_distribution",
serde_json::json!({ "symbol": symbol, "market": market }),
)
.await
}
pub async fn market_scanner(
&self,
req: MarketScannerRequest,
) -> Result<Option<ScannerResult>, TigerError> {
self.call_optional_versioned("market_scanner", req, Some(VERSION_V1)).await
}
pub async fn grab_quote_permission(&self) -> Result<Vec<QuotePermission>, TigerError> {
self.call_into("grab_quote_permission", serde_json::json!({})).await
}
pub async fn get_market_scanner_tags(&self, req: MarketScannerTagsRequest) -> Result<Option<MarketScannerTags>, TigerError> {
self.call_optional("market_scanner_tags", req).await
}
pub async fn get_quote_overnight(&self, req: QuoteOvernightRequest) -> Result<Vec<QuoteOvernight>, TigerError> {
self.call_into("quote_overnight", req).await
}
async fn call_optional<T, P>(&self, method: &str, params: P) -> Result<Option<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
self.call_optional_versioned(method, params, None).await
}
async fn call_optional_versioned<T, P>(
&self,
method: &str,
params: P,
version: Option<&str>,
) -> Result<Option<T>, TigerError>
where
T: serde::de::DeserializeOwned,
P: Serialize,
{
let biz = serde_json::to_string(¶ms)
.map_err(|e| TigerError::Config(format!("serialize biz params failed: {}", e)))?;
let req = match version {
Some(v) => ApiRequest::with_version(method, biz, v),
None => ApiRequest::new(method, biz),
};
let resp = self.http_client.execute_request(&req).await?;
match resp.data {
None => Ok(None),
Some(v) if v.is_null() => Ok(None),
Some(v) => {
let parsed: T = decode_value(v)?;
Ok(Some(parsed))
}
}
}
}
fn unmarshal_data<T>(data: Option<Value>) -> Result<T, TigerError>
where
T: serde::de::DeserializeOwned + Default,
{
match data {
None => Ok(T::default()),
Some(v) if v.is_null() => Ok(T::default()),
Some(v) => decode_value(v),
}
}
struct OptionContract {
symbol: String,
expiry: i64,
right: String,
strike: f64,
}
fn parse_expiry_to_ms(expiry: &str) -> Result<i64, TigerError> {
use chrono::NaiveDate;
let d = NaiveDate::parse_from_str(expiry, "%Y-%m-%d")
.map_err(|e| TigerError::Config(format!("invalid expiry {:?}: expected YYYY-MM-DD: {}", expiry, e)))?;
let dt = d
.and_hms_opt(0, 0, 0)
.ok_or_else(|| TigerError::Config(format!("invalid expiry date: {:?}", expiry)))?;
let utc = dt.and_utc();
Ok(utc.timestamp_millis())
}
fn parse_option_identifier(identifier: &str) -> Result<OptionContract, TigerError> {
let trimmed = identifier.trim();
let mut it = trimmed.splitn(2, ' ');
let symbol = it
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| TigerError::Config(format!("invalid option identifier: {:?}", identifier)))?
.to_string();
let rest = it
.next()
.map(str::trim)
.ok_or_else(|| TigerError::Config(format!("invalid option identifier: {:?}", identifier)))?;
if rest.len() < 15 {
return Err(TigerError::Config(format!("option code too short: {:?}", rest)));
}
let date_str = &rest[..6];
let date = chrono::NaiveDate::parse_from_str(date_str, "%y%m%d")
.map_err(|_| TigerError::Config(format!("invalid date in identifier: {:?}", date_str)))?;
let expiry = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| TigerError::Config("invalid date".into()))?
.and_utc()
.timestamp_millis();
let right_char = rest.as_bytes()[6];
let right = match right_char {
b'C' => "CALL",
b'P' => "PUT",
other => {
return Err(TigerError::Config(format!(
"invalid right character: {:?}",
other as char
)));
}
}
.to_string();
let strike_str = &rest[7..];
let mut strike_int: i64 = 0;
for c in strike_str.chars() {
if !c.is_ascii_digit() {
return Err(TigerError::Config(format!("invalid strike digits: {:?}", strike_str)));
}
strike_int = strike_int * 10 + (c as i64 - '0' as i64);
}
let strike = strike_int as f64 / 1000.0;
Ok(OptionContract {
symbol,
expiry,
right,
strike,
})
}
#[cfg(test)]
mod tests;