use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use serde_json::Value;
use crate::core::{
HttpClient, Credentials,
ExchangeId, ExchangeType, AccountType, Symbol,
ExchangeError, ExchangeResult,
Price, Kline, Ticker, OrderBook,
Order, Balance, AccountInfo, Position, FundingRate,
OrderRequest, CancelRequest,
BalanceQuery, PositionQuery, PositionModification,
OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
};
use crate::core::traits::{
ExchangeIdentity, MarketData, Trading, Account, Positions,
};
use crate::core::utils::WeightRateLimiter;
use crate::core::types::{SymbolInfo, SymbolInput};
use super::endpoints::{
TiingoUrls, TiingoEndpoint,
format_crypto_symbol, format_forex_symbol,
map_interval,
};
use super::auth::TiingoAuth;
use super::parser::TiingoParser;
pub struct TiingoConnector {
http: HttpClient,
auth: TiingoAuth,
urls: TiingoUrls,
rate_limiter: Arc<Mutex<WeightRateLimiter>>,
}
impl TiingoConnector {
pub async fn new(credentials: Credentials) -> ExchangeResult<Self> {
let auth = TiingoAuth::new(&credentials)?;
let urls = TiingoUrls::MAINNET;
let http = HttpClient::new(30_000)?;
let rate_limiter = Arc::new(Mutex::new(
WeightRateLimiter::new(5, Duration::from_secs(60))
));
Ok(Self {
http,
auth,
urls,
rate_limiter,
})
}
async fn rate_limit_wait(&self, weight: u32) {
loop {
let wait_time = {
let mut limiter = self.rate_limiter.lock().expect("Mutex poisoned");
if limiter.try_acquire(weight) {
return;
}
limiter.time_until_ready(weight)
};
if wait_time > Duration::ZERO {
tokio::time::sleep(wait_time).await;
}
}
}
async fn get(
&self,
endpoint: TiingoEndpoint,
ticker: Option<&str>,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
self.rate_limit_wait(1).await;
let base_url = self.urls.rest_url();
let url = endpoint.build_url(base_url, ticker);
let headers = self.auth.get_auth_header();
let query = if params.is_empty() {
String::new()
} else {
let qs: Vec<String> = params.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect();
format!("?{}", qs.join("&"))
};
let full_url = format!("{}{}", url, query);
let response = self.http.get(&full_url, &headers).await?;
Ok(response)
}
}
impl ExchangeIdentity for TiingoConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Tiingo
}
fn is_testnet(&self) -> bool {
false }
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::DataProvider
}
}
#[async_trait]
impl MarketData for TiingoConnector {
async fn get_price(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Price> {
let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let mut params = HashMap::new();
params.insert("columns".to_string(), "close".to_string());
let response = self.get(
TiingoEndpoint::IexPrices,
Some(&sym_str),
params,
).await?;
let klines = TiingoParser::parse_iex_prices(&response)?;
let latest = klines.last()
.ok_or_else(|| ExchangeError::Parse("No price data available".to_string()))?;
Ok(latest.close)
}
async fn get_orderbook(
&self,
_symbol: SymbolInput<'_>,
_limit: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo does not provide orderbook data - market data provider only".to_string()
))
}
async fn get_klines(
&self,
symbol: SymbolInput<'_>,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let resample_freq = map_interval(interval);
let mut params = HashMap::new();
params.insert("resampleFreq".to_string(), resample_freq.to_string());
let response = self.get(
TiingoEndpoint::IexPrices,
Some(&sym_str),
params,
).await?;
let mut klines = TiingoParser::parse_iex_prices(&response)?;
if let Some(lim) = limit {
let start = klines.len().saturating_sub(lim as usize);
klines = klines[start..].to_vec();
}
Ok(klines)
}
async fn get_ticker(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let response = self.get(
TiingoEndpoint::IexPrices,
Some(&sym_str),
HashMap::new(),
).await?;
let klines = TiingoParser::parse_iex_prices(&response)?;
if klines.is_empty() {
return Err(ExchangeError::Parse("No ticker data available".to_string()));
}
let latest = klines.last().expect("Klines should not be empty");
let high = klines.iter().map(|k| k.high).fold(f64::NEG_INFINITY, f64::max);
let low = klines.iter().map(|k| k.low).fold(f64::INFINITY, f64::min);
let volume: f64 = klines.iter().map(|k| k.volume).sum();
Ok(Ticker {
last_price: latest.close,
bid_price: None,
ask_price: None,
high_24h: Some(high),
low_24h: Some(low),
volume_24h: Some(volume),
quote_volume_24h: None,
price_change_24h: None,
price_change_percent_24h: None,
timestamp: latest.open_time,
})
}
async fn ping(&self) -> ExchangeResult<()> {
let response = self.get(
TiingoEndpoint::FundamentalsDefinitions,
None,
HashMap::new(),
).await?;
if response.is_array() || response.is_object() {
Ok(())
} else {
Err(ExchangeError::Network("Ping failed".to_string()))
}
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let response = self.get(TiingoEndpoint::CryptoMeta, None, HashMap::new()).await?;
let arr = response.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array of crypto tickers".to_string()))?;
let infos = arr.iter().filter_map(|item| {
let ticker = item.get("ticker")?.as_str()?.to_string();
let base = item.get("baseCurrency")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_uppercase();
let quote = item.get("quoteCurrency")
.and_then(|v| v.as_str())
.unwrap_or("USD")
.to_uppercase();
Some(SymbolInfo {
symbol: ticker,
base_asset: base,
quote_asset: quote,
status: "TRADING".to_string(),
price_precision: 8,
quantity_precision: 8,
min_quantity: None,
max_quantity: None,
tick_size: None,
step_size: None,
min_notional: None,
account_type,
})
}).collect();
Ok(infos)
}
}
#[async_trait]
impl Trading for TiingoConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
async fn get_order(
&self,
_symbol: &str,
_order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
async fn get_order_history(
&self,
_filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
}
#[async_trait]
impl Account for TiingoConnector {
async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
))
}
async fn get_account_info(
&self,
_account_type: AccountType,
) -> ExchangeResult<AccountInfo> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
))
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
))
}
}
#[async_trait]
impl Positions for TiingoConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
))
}
}
impl TiingoConnector {
pub async fn get_daily_prices(
&self,
ticker: &str,
start_date: Option<&str>,
end_date: Option<&str>,
) -> ExchangeResult<Vec<Kline>> {
let mut params = HashMap::new();
if let Some(start) = start_date {
params.insert("startDate".to_string(), start.to_string());
}
if let Some(end) = end_date {
params.insert("endDate".to_string(), end.to_string());
}
let response = self.get(
TiingoEndpoint::DailyPrices,
Some(ticker),
params,
).await?;
TiingoParser::parse_daily_prices(&response)
}
pub async fn get_crypto_top(
&self,
symbol: &Symbol,
) -> ExchangeResult<Ticker> {
let ticker_symbol = format_crypto_symbol(symbol);
let mut params = HashMap::new();
params.insert("tickers".to_string(), ticker_symbol);
let response = self.get(
TiingoEndpoint::CryptoTop,
None,
params,
).await?;
TiingoParser::parse_crypto_top(&response)
}
pub async fn get_crypto_prices(
&self,
symbol: &Symbol,
start_date: Option<&str>,
interval: &str,
) -> ExchangeResult<Vec<Kline>> {
let ticker_symbol = format_crypto_symbol(symbol);
let resample_freq = map_interval(interval);
let mut params = HashMap::new();
params.insert("tickers".to_string(), ticker_symbol);
params.insert("resampleFreq".to_string(), resample_freq.to_string());
if let Some(start) = start_date {
params.insert("startDate".to_string(), start.to_string());
}
let response = self.get(
TiingoEndpoint::CryptoPrices,
None,
params,
).await?;
TiingoParser::parse_crypto_prices(&response)
}
pub async fn get_forex_top(
&self,
symbol: &Symbol,
) -> ExchangeResult<Ticker> {
let ticker_symbol = format_forex_symbol(symbol);
let response = self.get(
TiingoEndpoint::ForexTop,
Some(&ticker_symbol),
HashMap::new(),
).await?;
TiingoParser::parse_forex_top(&response)
}
pub async fn get_forex_prices(
&self,
symbol: &Symbol,
start_date: Option<&str>,
interval: &str,
) -> ExchangeResult<Vec<Kline>> {
let ticker_symbol = format_forex_symbol(symbol);
let resample_freq = map_interval(interval);
let mut params = HashMap::new();
params.insert("resampleFreq".to_string(), resample_freq.to_string());
if let Some(start) = start_date {
params.insert("startDate".to_string(), start.to_string());
}
let response = self.get(
TiingoEndpoint::ForexPrices,
Some(&ticker_symbol),
params,
).await?;
TiingoParser::parse_forex_prices(&response)
}
}