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;
use super::endpoints::{FinnhubUrls, FinnhubEndpoint, format_symbol, map_resolution};
use super::auth::FinnhubAuth;
use super::parser::FinnhubParser;
pub struct FinnhubConnector {
http: HttpClient,
auth: FinnhubAuth,
urls: FinnhubUrls,
rate_limiter: Arc<Mutex<WeightRateLimiter>>,
}
impl FinnhubConnector {
pub async fn new(credentials: Credentials) -> ExchangeResult<Self> {
let auth = FinnhubAuth::new(&credentials)?;
let urls = FinnhubUrls::MAINNET;
let http = HttpClient::new(30_000)?;
let rate_limiter = Arc::new(Mutex::new(
WeightRateLimiter::new(60, 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: FinnhubEndpoint,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
self.rate_limit_wait(1).await;
let base_url = self.urls.rest_url();
let path = endpoint.path();
let mut all_params = params;
self.auth.add_to_params(&mut all_params);
let query = if all_params.is_empty() {
String::new()
} else {
let qs: Vec<String> = all_params.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect();
format!("?{}", qs.join("&"))
};
let url = format!("{}{}{}", base_url, path, query);
let response = self.http.get(&url, &HashMap::new()).await?;
Ok(response)
}
pub async fn get_etf_holdings(
&self,
symbol: &str,
date: Option<&str>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
if let Some(d) = date {
params.insert("date".to_string(), d.to_string());
}
self.get(FinnhubEndpoint::EtfHoldings, params).await
}
pub async fn get_etf_profile(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
self.get(FinnhubEndpoint::EtfProfile, params).await
}
pub async fn get_etf_country_exposure(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
self.get(FinnhubEndpoint::EtfCountryExposure, params).await
}
pub async fn get_etf_sector_exposure(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
self.get(FinnhubEndpoint::EtfSectorExposure, params).await
}
pub async fn get_ipo_calendar(&self, from: &str, to: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("from".to_string(), from.to_string());
params.insert("to".to_string(), to.to_string());
self.get(FinnhubEndpoint::IpoCalendar, params).await
}
pub async fn get_earnings_surprise(
&self,
symbol: &str,
limit: Option<u32>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
if let Some(l) = limit {
params.insert("limit".to_string(), l.to_string());
}
self.get(FinnhubEndpoint::EarningsSurprise, params).await
}
pub async fn get_social_sentiment(
&self,
symbol: &str,
from: Option<&str>,
to: Option<&str>,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_uppercase());
if let Some(f) = from {
params.insert("from".to_string(), f.to_string());
}
if let Some(t) = to {
params.insert("to".to_string(), t.to_string());
}
self.get(FinnhubEndpoint::SocialSentiment, params).await
}
pub async fn get_crypto_profile(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_string());
self.get(FinnhubEndpoint::CryptoProfile, params).await
}
}
impl ExchangeIdentity for FinnhubConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Finnhub
}
fn is_testnet(&self) -> bool {
false }
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::Cex }
}
#[async_trait]
impl MarketData for FinnhubConnector {
async fn get_price(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Price> {
let ticker_symbol = format_symbol(&symbol.base);
let mut params = HashMap::new();
params.insert("symbol".to_string(), ticker_symbol);
let response = self.get(
FinnhubEndpoint::Quote,
params,
).await?;
FinnhubParser::parse_price(&response)
}
async fn get_orderbook(
&self,
symbol: Symbol,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
let ticker_symbol = format_symbol(&symbol.base);
let mut params = HashMap::new();
params.insert("symbol".to_string(), ticker_symbol);
let response = self.get(
FinnhubEndpoint::BidAsk,
params,
).await?;
FinnhubParser::parse_orderbook(&response)
}
async fn get_klines(
&self,
symbol: Symbol,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
let ticker_symbol = format_symbol(&symbol.base);
let resolution = map_resolution(interval);
let to = chrono::Utc::now().timestamp();
let from = if let Some(lim) = limit {
let seconds_per_candle = match resolution {
"1" => 60, "5" => 300, "15" => 900, "30" => 1800, "60" => 3600, "D" => 86400, "W" => 604800, "M" => 2592000, _ => 86400,
};
to - (lim as i64 * seconds_per_candle)
} else {
to - (30 * 86400)
};
let mut params = HashMap::new();
params.insert("symbol".to_string(), ticker_symbol);
params.insert("resolution".to_string(), resolution.to_string());
params.insert("from".to_string(), from.to_string());
params.insert("to".to_string(), to.to_string());
let response = self.get(
FinnhubEndpoint::StockCandles,
params,
).await?;
FinnhubParser::parse_klines(&response)
}
async fn get_ticker(
&self,
symbol: Symbol,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
let ticker_symbol = format_symbol(&symbol.base);
let mut params = HashMap::new();
params.insert("symbol".to_string(), ticker_symbol.clone());
let response = self.get(
FinnhubEndpoint::Quote,
params,
).await?;
let mut ticker = FinnhubParser::parse_ticker(&response)?;
ticker.symbol = ticker_symbol;
Ok(ticker)
}
async fn ping(&self) -> ExchangeResult<()> {
let mut params = HashMap::new();
params.insert("exchange".to_string(), "US".to_string());
let response = self.get(
FinnhubEndpoint::MarketStatus,
params,
).await?;
FinnhubParser::check_error(&response)?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let mut params = HashMap::new();
params.insert("exchange".to_string(), "US".to_string());
let response = self.get(FinnhubEndpoint::StockSymbols, params).await?;
let arr = response.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array of symbols".to_string()))?;
let infos = arr.iter().filter_map(|item| {
let symbol = item.get("symbol")?.as_str()?.to_string();
let currency = item.get("currency")
.and_then(|v| v.as_str())
.unwrap_or("USD")
.to_uppercase();
Some(SymbolInfo {
symbol: symbol.clone(),
base_asset: symbol,
quote_asset: currency,
status: "TRADING".to_string(),
price_precision: 2,
quantity_precision: 0,
min_quantity: Some(1.0),
max_quantity: None,
tick_size: None,
step_size: Some(1.0),
min_notional: None,
account_type,
})
}).collect();
Ok(infos)
}
}
#[async_trait]
impl Trading for FinnhubConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"Finnhub 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(
"Finnhub 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(
"Finnhub 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(
"Finnhub 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(
"Finnhub is a data provider, not an exchange. Trading is not supported.".to_string()
))
}
}
#[async_trait]
impl Account for FinnhubConnector {
async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
Err(ExchangeError::UnsupportedOperation(
"Finnhub 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(
"Finnhub 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(
"Finnhub is a data provider, not an exchange. Account operations are not supported.".to_string()
))
}
}
#[async_trait]
impl Positions for FinnhubConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"Finnhub is a data provider, not an exchange. Position operations are not supported.".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"Finnhub is a data provider, not an exchange. Position operations are not supported.".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"Finnhub is a data provider, not an exchange. Position operations are not supported.".to_string()
))
}
}