1use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14use std::time::Duration;
15
16use async_trait::async_trait;
17use serde_json::Value;
18
19use crate::core::{
20 HttpClient, Credentials,
21 ExchangeId, ExchangeType, AccountType, Symbol,
22 ExchangeError, ExchangeResult,
23 Price, Kline, Ticker, OrderBook,
24 Order, Balance, AccountInfo, Position, FundingRate,
25 OrderRequest, CancelRequest,
26 BalanceQuery, PositionQuery, PositionModification,
27 OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
28};
29use crate::core::traits::{
30 ExchangeIdentity, MarketData, Trading, Account, Positions,
31};
32use crate::core::utils::WeightRateLimiter;
33use crate::core::types::{SymbolInfo, SymbolInput};
34
35use super::endpoints::{
36 TiingoUrls, TiingoEndpoint,
37 format_crypto_symbol, format_forex_symbol,
38 map_interval,
39};
40use super::auth::TiingoAuth;
41use super::parser::TiingoParser;
42
43pub struct TiingoConnector {
49 http: HttpClient,
51 auth: TiingoAuth,
53 urls: TiingoUrls,
55 rate_limiter: Arc<Mutex<WeightRateLimiter>>,
57}
58
59impl TiingoConnector {
60 pub async fn new(credentials: Credentials) -> ExchangeResult<Self> {
65 let auth = TiingoAuth::new(&credentials)?;
66 let urls = TiingoUrls::MAINNET;
67 let http = HttpClient::new(30_000)?; let rate_limiter = Arc::new(Mutex::new(
72 WeightRateLimiter::new(5, Duration::from_secs(60))
73 ));
74
75 Ok(Self {
76 http,
77 auth,
78 urls,
79 rate_limiter,
80 })
81 }
82
83 async fn rate_limit_wait(&self, weight: u32) {
89 loop {
90 let wait_time = {
91 let mut limiter = self.rate_limiter.lock().expect("Mutex poisoned");
92 if limiter.try_acquire(weight) {
93 return;
94 }
95 limiter.time_until_ready(weight)
96 };
97
98 if wait_time > Duration::ZERO {
99 tokio::time::sleep(wait_time).await;
100 }
101 }
102 }
103
104 async fn get(
106 &self,
107 endpoint: TiingoEndpoint,
108 ticker: Option<&str>,
109 params: HashMap<String, String>,
110 ) -> ExchangeResult<Value> {
111 self.rate_limit_wait(1).await;
113
114 let base_url = self.urls.rest_url();
115 let url = endpoint.build_url(base_url, ticker);
116
117 let headers = self.auth.get_auth_header();
119
120 let query = if params.is_empty() {
122 String::new()
123 } else {
124 let qs: Vec<String> = params.iter()
125 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
126 .collect();
127 format!("?{}", qs.join("&"))
128 };
129
130 let full_url = format!("{}{}", url, query);
131
132 let response = self.http.get(&full_url, &headers).await?;
134
135 Ok(response)
136 }
137}
138
139impl ExchangeIdentity for TiingoConnector {
144 fn exchange_id(&self) -> ExchangeId {
145 ExchangeId::Tiingo
146 }
147
148 fn is_testnet(&self) -> bool {
149 false }
151
152 fn supported_account_types(&self) -> Vec<AccountType> {
153 vec![AccountType::Spot]
155 }
156
157 fn exchange_type(&self) -> ExchangeType {
158 ExchangeType::DataProvider
159 }
160}
161
162#[async_trait]
167impl MarketData for TiingoConnector {
168 async fn get_price(
170 &self,
171 symbol: SymbolInput<'_>,
172 _account_type: AccountType,
173 ) -> ExchangeResult<Price> {
174 let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
175 let mut params = HashMap::new();
176 params.insert("columns".to_string(), "close".to_string());
177
178 let response = self.get(
179 TiingoEndpoint::IexPrices,
180 Some(&sym_str),
181 params,
182 ).await?;
183
184 let klines = TiingoParser::parse_iex_prices(&response)?;
186 let latest = klines.last()
187 .ok_or_else(|| ExchangeError::Parse("No price data available".to_string()))?;
188
189 Ok(latest.close)
190 }
191
192 async fn get_orderbook(
194 &self,
195 _symbol: SymbolInput<'_>,
196 _limit: Option<u16>,
197 _account_type: AccountType,
198 ) -> ExchangeResult<OrderBook> {
199 Err(ExchangeError::UnsupportedOperation(
200 "Tiingo does not provide orderbook data - market data provider only".to_string()
201 ))
202 }
203
204 async fn get_klines(
206 &self,
207 symbol: SymbolInput<'_>,
208 interval: &str,
209 limit: Option<u16>,
210 _account_type: AccountType,
211 _end_time: Option<i64>,
212 ) -> ExchangeResult<Vec<Kline>> {
213 let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
214 let resample_freq = map_interval(interval);
215
216 let mut params = HashMap::new();
217 params.insert("resampleFreq".to_string(), resample_freq.to_string());
218
219 let response = self.get(
220 TiingoEndpoint::IexPrices,
221 Some(&sym_str),
222 params,
223 ).await?;
224
225 let mut klines = TiingoParser::parse_iex_prices(&response)?;
226
227 if let Some(lim) = limit {
229 let start = klines.len().saturating_sub(lim as usize);
230 klines = klines[start..].to_vec();
231 }
232
233 Ok(klines)
234 }
235
236 async fn get_ticker(
238 &self,
239 symbol: SymbolInput<'_>,
240 _account_type: AccountType,
241 ) -> ExchangeResult<Ticker> {
242 let sym_str: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
243 let response = self.get(
244 TiingoEndpoint::IexPrices,
245 Some(&sym_str),
246 HashMap::new(),
247 ).await?;
248
249 let klines = TiingoParser::parse_iex_prices(&response)?;
250
251 if klines.is_empty() {
252 return Err(ExchangeError::Parse("No ticker data available".to_string()));
253 }
254
255 let latest = klines.last().expect("Klines should not be empty");
257 let high = klines.iter().map(|k| k.high).fold(f64::NEG_INFINITY, f64::max);
258 let low = klines.iter().map(|k| k.low).fold(f64::INFINITY, f64::min);
259 let volume: f64 = klines.iter().map(|k| k.volume).sum();
260
261 Ok(Ticker {
262 symbol: sym_str,
263 last_price: latest.close,
264 bid_price: None,
265 ask_price: None,
266 high_24h: Some(high),
267 low_24h: Some(low),
268 volume_24h: Some(volume),
269 quote_volume_24h: None,
270 price_change_24h: None,
271 price_change_percent_24h: None,
272 timestamp: latest.open_time,
273 })
274 }
275
276 async fn ping(&self) -> ExchangeResult<()> {
278 let response = self.get(
280 TiingoEndpoint::FundamentalsDefinitions,
281 None,
282 HashMap::new(),
283 ).await?;
284
285 if response.is_array() || response.is_object() {
286 Ok(())
287 } else {
288 Err(ExchangeError::Network("Ping failed".to_string()))
289 }
290 }
291
292 async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
294 let response = self.get(TiingoEndpoint::CryptoMeta, None, HashMap::new()).await?;
297
298 let arr = response.as_array()
299 .ok_or_else(|| ExchangeError::Parse("Expected array of crypto tickers".to_string()))?;
300
301 let infos = arr.iter().filter_map(|item| {
302 let ticker = item.get("ticker")?.as_str()?.to_string();
303 let base = item.get("baseCurrency")
304 .and_then(|v| v.as_str())
305 .unwrap_or("")
306 .to_uppercase();
307 let quote = item.get("quoteCurrency")
308 .and_then(|v| v.as_str())
309 .unwrap_or("USD")
310 .to_uppercase();
311
312 Some(SymbolInfo {
313 symbol: ticker,
314 base_asset: base,
315 quote_asset: quote,
316 status: "TRADING".to_string(),
317 price_precision: 8,
318 quantity_precision: 8,
319 min_quantity: None,
320 max_quantity: None,
321 tick_size: None,
322 step_size: None,
323 min_notional: None,
324 account_type,
325 })
326 }).collect();
327
328 Ok(infos)
329 }
330}
331
332#[async_trait]
337impl Trading for TiingoConnector {
338 async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
339 Err(ExchangeError::UnsupportedOperation(
340 "Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
341 ))
342 }
343
344 async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
345 Err(ExchangeError::UnsupportedOperation(
346 "Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
347 ))
348 }
349
350 async fn get_order(
351 &self,
352 _symbol: &str,
353 _order_id: &str,
354 _account_type: AccountType,
355 ) -> ExchangeResult<Order> {
356 Err(ExchangeError::UnsupportedOperation(
357 "Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
358 ))
359 }
360
361 async fn get_open_orders(
362 &self,
363 _symbol: Option<&str>,
364 _account_type: AccountType,
365 ) -> ExchangeResult<Vec<Order>> {
366 Err(ExchangeError::UnsupportedOperation(
367 "Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
368 ))
369 }
370
371 async fn get_order_history(
372 &self,
373 _filter: OrderHistoryFilter,
374 _account_type: AccountType,
375 ) -> ExchangeResult<Vec<Order>> {
376 Err(ExchangeError::UnsupportedOperation(
377 "Tiingo is a data provider, not an exchange. Trading is not supported.".to_string()
378 ))
379 }
380}
381
382#[async_trait]
387impl Account for TiingoConnector {
388 async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
389 Err(ExchangeError::UnsupportedOperation(
390 "Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
391 ))
392
393 }
394
395 async fn get_account_info(
396 &self,
397 _account_type: AccountType,
398 ) -> ExchangeResult<AccountInfo> {
399 Err(ExchangeError::UnsupportedOperation(
400 "Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
401 ))
402 }
403
404 async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
405 Err(ExchangeError::UnsupportedOperation(
406 "Tiingo is a data provider, not an exchange. Account operations are not supported.".to_string()
407 ))
408 }
409}
410
411#[async_trait]
416impl Positions for TiingoConnector {
417 async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
418 Err(ExchangeError::UnsupportedOperation(
419 "Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
420 ))
421 }
422
423 async fn get_funding_rate(
424 &self,
425 _symbol: &str,
426 _account_type: AccountType,
427 ) -> ExchangeResult<FundingRate> {
428 Err(ExchangeError::UnsupportedOperation(
429 "Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
430 ))
431 }
432
433 async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
434 Err(ExchangeError::UnsupportedOperation(
435 "Tiingo is a data provider, not an exchange. Position tracking is not supported.".to_string()
436 ))
437 }
438}
439
440impl TiingoConnector {
445 pub async fn get_daily_prices(
447 &self,
448 ticker: &str,
449 start_date: Option<&str>,
450 end_date: Option<&str>,
451 ) -> ExchangeResult<Vec<Kline>> {
452 let mut params = HashMap::new();
453
454 if let Some(start) = start_date {
455 params.insert("startDate".to_string(), start.to_string());
456 }
457 if let Some(end) = end_date {
458 params.insert("endDate".to_string(), end.to_string());
459 }
460
461 let response = self.get(
462 TiingoEndpoint::DailyPrices,
463 Some(ticker),
464 params,
465 ).await?;
466
467 TiingoParser::parse_daily_prices(&response)
468 }
469
470 pub async fn get_crypto_top(
472 &self,
473 symbol: &Symbol,
474 ) -> ExchangeResult<Ticker> {
475 let ticker_symbol = format_crypto_symbol(symbol);
476
477 let mut params = HashMap::new();
478 params.insert("tickers".to_string(), ticker_symbol);
479
480 let response = self.get(
481 TiingoEndpoint::CryptoTop,
482 None,
483 params,
484 ).await?;
485
486 TiingoParser::parse_crypto_top(&response)
487 }
488
489 pub async fn get_crypto_prices(
491 &self,
492 symbol: &Symbol,
493 start_date: Option<&str>,
494 interval: &str,
495 ) -> ExchangeResult<Vec<Kline>> {
496 let ticker_symbol = format_crypto_symbol(symbol);
497 let resample_freq = map_interval(interval);
498
499 let mut params = HashMap::new();
500 params.insert("tickers".to_string(), ticker_symbol);
501 params.insert("resampleFreq".to_string(), resample_freq.to_string());
502
503 if let Some(start) = start_date {
504 params.insert("startDate".to_string(), start.to_string());
505 }
506
507 let response = self.get(
508 TiingoEndpoint::CryptoPrices,
509 None,
510 params,
511 ).await?;
512
513 TiingoParser::parse_crypto_prices(&response)
514 }
515
516 pub async fn get_forex_top(
518 &self,
519 symbol: &Symbol,
520 ) -> ExchangeResult<Ticker> {
521 let ticker_symbol = format_forex_symbol(symbol);
522
523 let response = self.get(
524 TiingoEndpoint::ForexTop,
525 Some(&ticker_symbol),
526 HashMap::new(),
527 ).await?;
528
529 TiingoParser::parse_forex_top(&response)
530 }
531
532 pub async fn get_forex_prices(
534 &self,
535 symbol: &Symbol,
536 start_date: Option<&str>,
537 interval: &str,
538 ) -> ExchangeResult<Vec<Kline>> {
539 let ticker_symbol = format_forex_symbol(symbol);
540 let resample_freq = map_interval(interval);
541
542 let mut params = HashMap::new();
543 params.insert("resampleFreq".to_string(), resample_freq.to_string());
544
545 if let Some(start) = start_date {
546 params.insert("startDate".to_string(), start.to_string());
547 }
548
549 let response = self.get(
550 TiingoEndpoint::ForexPrices,
551 Some(&ticker_symbol),
552 params,
553 ).await?;
554
555 TiingoParser::parse_forex_prices(&response)
556 }
557}