use async_trait::async_trait;
use reqwest::Client;
use std::collections::HashMap;
use crate::core::types::{
Symbol, AccountType, Price, Ticker, Kline, OrderBook, FundingRate,
ExchangeId, ExchangeError, ExchangeResult,
Order, Balance, AccountInfo, Position, SymbolInfo, SymbolInput,
OrderRequest, CancelRequest, OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
BalanceQuery, PositionQuery, PositionModification,
};
use crate::core::traits::{ExchangeIdentity, MarketData, Trading, Account, Positions};
use super::endpoints::*;
use super::auth::*;
use super::parser::*;
pub struct CryptoCompareConnector {
client: Client,
auth: CryptoCompareAuth,
endpoints: CryptoCompareEndpoints,
}
impl CryptoCompareConnector {
pub fn new(auth: CryptoCompareAuth) -> Self {
Self {
client: Client::new(),
auth,
endpoints: CryptoCompareEndpoints::default(),
}
}
pub fn from_env() -> Self {
Self::new(CryptoCompareAuth::from_env())
}
pub fn public() -> Self {
Self::new(CryptoCompareAuth::public())
}
async fn get(
&self,
endpoint: CryptoCompareEndpoint,
mut params: HashMap<String, String>,
) -> ExchangeResult<serde_json::Value> {
let url = format!("{}{}", self.endpoints.rest_base, endpoint.path());
self.auth.sign_query(&mut params);
let response = self
.client
.get(&url)
.query(¶ms)
.send()
.await
.map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;
if !response.status().is_success() {
let status_code = response.status().as_u16() as i32;
let message = format!(
"HTTP {} - {}",
response.status(),
response.text().await.unwrap_or_default()
);
return Err(ExchangeError::Api { code: status_code, message });
}
response
.json()
.await
.map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))
}
}
impl ExchangeIdentity for CryptoCompareConnector {
fn exchange_name(&self) -> &'static str {
"cryptocompare"
}
fn exchange_id(&self) -> ExchangeId {
ExchangeId::CryptoCompare
}
fn is_testnet(&self) -> bool {
false }
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
}
#[async_trait]
impl MarketData for CryptoCompareConnector {
async fn get_price(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Price> {
let symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let (fsym, tsym) = symbol.split_once('-')
.or_else(|| symbol.split_once('/'))
.or_else(|| symbol.split_once('_'))
.map(|(b, q)| (b.to_uppercase(), q.to_uppercase()))
.unwrap_or_else(|| (symbol.to_uppercase(), "USD".to_string()));
let mut params = HashMap::new();
params.insert("fsym".to_string(), fsym);
params.insert("tsyms".to_string(), tsym.clone());
let response = self.get(CryptoCompareEndpoint::Price, params).await?;
CryptoCompareParser::parse_price(&response, &tsym)
}
async fn get_ticker(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
let symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let (fsym, tsym) = symbol.split_once('-')
.or_else(|| symbol.split_once('/'))
.or_else(|| symbol.split_once('_'))
.map(|(b, q)| (b.to_uppercase(), q.to_uppercase()))
.unwrap_or_else(|| (symbol.to_uppercase(), "USD".to_string()));
let mut params = HashMap::new();
params.insert("fsyms".to_string(), fsym.clone());
params.insert("tsyms".to_string(), tsym.clone());
let response = self.get(CryptoCompareEndpoint::PriceMultiFull, params).await?;
CryptoCompareParser::parse_ticker(&response, &fsym, &tsym)
}
async fn get_orderbook(
&self,
_symbol: SymbolInput<'_>,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare orderbook data requires paid tier and WebSocket connection - not available via REST API".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 symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let (fsym, tsym) = symbol.split_once('-')
.or_else(|| symbol.split_once('/'))
.or_else(|| symbol.split_once('_'))
.map(|(b, q)| (b.to_uppercase(), q.to_uppercase()))
.unwrap_or_else(|| (symbol.to_uppercase(), "USD".to_string()));
let (endpoint, aggregate) = map_interval_aggregate(interval);
let mut params = HashMap::new();
params.insert("fsym".to_string(), fsym);
params.insert("tsym".to_string(), tsym);
params.insert("aggregate".to_string(), aggregate.to_string());
if let Some(lim) = limit {
params.insert("limit".to_string(), lim.to_string());
} else {
params.insert("limit".to_string(), "100".to_string());
}
let response = self.get(endpoint, params).await?;
CryptoCompareParser::parse_klines(&response)
}
async fn ping(&self) -> ExchangeResult<()> {
let mut params = HashMap::new();
params.insert("fsym".to_string(), "BTC".to_string());
params.insert("tsyms".to_string(), "USD".to_string());
let _ = self.get(CryptoCompareEndpoint::Price, params).await?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let response = self.get(CryptoCompareEndpoint::CoinList, HashMap::new()).await?;
let symbols = CryptoCompareParser::parse_symbols(&response)?;
let infos = symbols
.into_iter()
.map(|symbol| SymbolInfo {
symbol: symbol.clone(),
base_asset: symbol,
quote_asset: "USD".to_string(), 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 CryptoCompareConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - trading not supported".to_string()
))
}
async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - trading not supported".to_string()
))
}
async fn get_order(
&self,
_symbol: &str,
_order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - trading not supported".to_string()
))
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - trading not supported".to_string()
))
}
async fn get_order_history(
&self,
_filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - trading not supported".to_string()
))
}
}
#[async_trait]
impl Account for CryptoCompareConnector {
async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - account operations not supported".to_string()
))
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - account operations not supported".to_string()
))
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - account operations not supported".to_string()
))
}
}
#[async_trait]
impl Positions for CryptoCompareConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - position tracking not supported".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - position tracking not supported".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"CryptoCompare is a data provider - position tracking not supported".to_string()
))
}
}
impl CryptoCompareConnector {
pub async fn get_historical_price(
&self,
symbol: Symbol,
timestamp: i64,
) -> ExchangeResult<f64> {
let (fsym, tsym) = format_symbol(&symbol);
let mut params = HashMap::new();
params.insert("fsym".to_string(), fsym);
params.insert("tsyms".to_string(), tsym.clone());
params.insert("ts".to_string(), (timestamp / 1000).to_string());
let response = self.get(CryptoCompareEndpoint::PriceHistorical, params).await?;
CryptoCompareParser::parse_price(&response, &tsym)
}
pub async fn get_top_exchanges(
&self,
symbol: Symbol,
limit: Option<u16>,
) -> ExchangeResult<serde_json::Value> {
let (fsym, tsym) = format_symbol(&symbol);
let mut params = HashMap::new();
params.insert("fsym".to_string(), fsym);
params.insert("tsym".to_string(), tsym);
if let Some(lim) = limit {
params.insert("limit".to_string(), lim.to_string());
}
self.get(CryptoCompareEndpoint::TopExchanges, params).await
}
pub async fn get_rate_limit(&self) -> ExchangeResult<serde_json::Value> {
if !self.auth.has_key() {
return Err(ExchangeError::Auth(
"API key required to check rate limits".to_string()
));
}
self.get(CryptoCompareEndpoint::RateLimit, HashMap::new()).await
}
}