use async_trait::async_trait;
use reqwest::Client;
use std::collections::HashMap;
use serde_json::Value;
use crate::core::types::*;
use crate::core::traits::*;
use super::endpoints::*;
use super::auth::*;
use super::parser::*;
pub struct TwelvedataConnector {
client: Client,
auth: TwelvedataAuth,
urls: TwelvedataUrls,
}
impl TwelvedataConnector {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
client: Client::new(),
auth: TwelvedataAuth::new(api_key),
urls: TwelvedataUrls::default(),
}
}
pub fn from_env() -> Self {
Self {
client: Client::new(),
auth: TwelvedataAuth::from_env(),
urls: TwelvedataUrls::default(),
}
}
pub fn demo() -> Self {
Self {
client: Client::new(),
auth: TwelvedataAuth::demo(),
urls: TwelvedataUrls::default(),
}
}
async fn get(
&self,
endpoint: TwelvedataEndpoint,
mut params: HashMap<String, String>,
) -> ExchangeResult<Value> {
let url = format!("{}{}", self.urls.rest, endpoint.path());
let mut headers = HashMap::new();
self.auth.sign_headers(&mut headers);
if !self.auth.has_credentials() || headers.is_empty() {
self.auth.add_query_param(&mut params);
}
let mut request = self.client.get(&url);
for (key, value) in headers {
request = request.header(key, value);
}
if !params.is_empty() {
request = request.query(¶ms);
}
let response = request
.send()
.await
.map_err(|e| ExchangeError::Network(e.to_string()))?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return match status.as_u16() {
401 => Err(ExchangeError::Auth("Invalid API key".to_string())),
403 => Err(ExchangeError::PermissionDenied(
"Endpoint requires higher tier plan".to_string(),
)),
429 => Err(ExchangeError::RateLimitExceeded {
retry_after: None,
message: "Rate limit exceeded".to_string(),
}),
_ => Err(ExchangeError::Http(format!(
"HTTP {}: {}",
status.as_u16(),
error_text
))),
};
}
response
.json::<Value>()
.await
.map_err(|e| ExchangeError::Parse(e.to_string()))
}
pub async fn get_realtime_price(&self, symbol: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.to_string());
self.get(TwelvedataEndpoint::RealTimePrice, params).await
}
pub async fn get_complex_data(&self, params: HashMap<String, String>) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::ComplexData, params).await
}
pub async fn get_mutual_funds_list(
&self,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::MutualFundsList, params).await
}
pub async fn get_bonds_list(
&self,
params: HashMap<String, String>,
) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::BondsList, params).await
}
}
#[async_trait]
impl ExchangeIdentity for TwelvedataConnector {
fn exchange_id(&self) -> ExchangeId {
ExchangeId::Twelvedata
}
fn exchange_name(&self) -> &'static str {
"Twelvedata"
}
fn exchange_type(&self) -> ExchangeType {
ExchangeType::DataProvider
}
fn is_testnet(&self) -> bool {
self.auth.is_demo()
}
fn supported_account_types(&self) -> Vec<AccountType> {
vec![AccountType::Spot]
}
}
#[async_trait]
impl MarketData for TwelvedataConnector {
async fn get_price(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Price> {
if !self.auth.has_credentials() || self.auth.is_demo() {
return Err(ExchangeError::NotSupported(
"Twelvedata requires a real API key — set TWELVEDATA_API_KEY env. \
Demo key is scope-limited and returns HTTP 401 for most endpoints. \
Sign up at https://twelvedata.com/ for a free tier key."
.to_string(),
));
}
let symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol);
let response = self.get(TwelvedataEndpoint::Price, params).await?;
TwelvedataParser::parse_price(&response)
}
async fn get_orderbook(
&self,
_symbol: SymbolInput<'_>,
_depth: Option<u16>,
_account_type: AccountType,
) -> ExchangeResult<OrderBook> {
Err(ExchangeError::NotSupported(
"Twelvedata does not expose order book depth — stocks/data provider only. \
Use a dedicated exchange connector for L2 order book data."
.to_string(),
))
}
async fn get_klines(
&self,
symbol: SymbolInput<'_>,
interval: &str,
limit: Option<u16>,
_account_type: AccountType,
_end_time: Option<i64>,
) -> ExchangeResult<Vec<Kline>> {
if !self.auth.has_credentials() || self.auth.is_demo() {
return Err(ExchangeError::NotSupported(
"Twelvedata requires a real API key — set TWELVEDATA_API_KEY env. \
Demo key is scope-limited and returns HTTP 401 for most endpoints. \
Sign up at https://twelvedata.com/ for a free tier key."
.to_string(),
));
}
let symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol);
params.insert("interval".to_string(), map_interval(interval).to_string());
if let Some(outputsize) = limit {
params.insert("outputsize".to_string(), outputsize.to_string());
}
let response = self.get(TwelvedataEndpoint::TimeSeries, params).await?;
TwelvedataParser::parse_klines(&response)
}
async fn get_ticker(
&self,
symbol: SymbolInput<'_>,
_account_type: AccountType,
) -> ExchangeResult<Ticker> {
if !self.auth.has_credentials() || self.auth.is_demo() {
return Err(ExchangeError::NotSupported(
"Twelvedata requires a real API key — set TWELVEDATA_API_KEY env. \
Demo key is scope-limited and returns HTTP 401 for most endpoints. \
Sign up at https://twelvedata.com/ for a free tier key."
.to_string(),
));
}
let symbol: String = match symbol { SymbolInput::Raw(s) => s.to_string(), SymbolInput::Canonical(c) => c.to_concat() };
let mut params = HashMap::new();
params.insert("symbol".to_string(), symbol.clone());
let response = self.get(TwelvedataEndpoint::Quote, params).await?;
TwelvedataParser::parse_ticker(&response, &symbol)
}
async fn ping(&self) -> ExchangeResult<()> {
if !self.auth.has_credentials() || self.auth.is_demo() {
return Err(ExchangeError::NotSupported(
"Twelvedata requires a real API key — set TWELVEDATA_API_KEY env."
.to_string(),
));
}
let mut params = HashMap::new();
params.insert("symbol".to_string(), "AAPL".to_string());
self.get(TwelvedataEndpoint::Price, params).await?;
Ok(())
}
async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let mut params = HashMap::new();
params.insert("country".to_string(), "United States".to_string());
params.insert("show_plan".to_string(), "false".to_string());
let response = self.get(TwelvedataEndpoint::Stocks, params).await?;
let data = response.get("data")
.and_then(|d| d.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing data array in stocks response".to_string()))?;
let infos = data.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 TwelvedataConnector {
async fn place_order(&self, _req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no trading capabilities".to_string()
))
}
async fn cancel_order(&self, _req: CancelRequest) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no trading capabilities".to_string()
))
}
async fn get_order(
&self,
_symbol: &str,
_order_id: &str,
_account_type: AccountType,
) -> ExchangeResult<Order> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no trading capabilities".to_string()
))
}
async fn get_open_orders(
&self,
_symbol: Option<&str>,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no trading capabilities".to_string()
))
}
async fn get_order_history(
&self,
_filter: OrderHistoryFilter,
_account_type: AccountType,
) -> ExchangeResult<Vec<Order>> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no trading capabilities".to_string()
))
}
}
#[async_trait]
impl Account for TwelvedataConnector {
async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no account/balance information".to_string(),
))
}
async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no account information".to_string(),
))
}
async fn get_fees(&self, _symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no account/balance information".to_string()
))
}
}
#[async_trait]
impl Positions for TwelvedataConnector {
async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no positions (no trading)".to_string()
))
}
async fn get_funding_rate(
&self,
_symbol: &str,
_account_type: AccountType,
) -> ExchangeResult<FundingRate> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no positions (no trading)".to_string()
))
}
async fn modify_position(&self, _req: PositionModification) -> ExchangeResult<()> {
Err(ExchangeError::UnsupportedOperation(
"Twelvedata is a data provider - no positions (no trading)".to_string()
))
}
}
impl TwelvedataConnector {
pub async fn symbol_search(&self, query: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), query.to_string());
self.get(TwelvedataEndpoint::SymbolSearch, params).await
}
pub async fn get_stocks(&self) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::Stocks, HashMap::new())
.await
}
pub async fn get_forex_pairs(&self) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::ForexPairs, HashMap::new())
.await
}
pub async fn get_cryptocurrencies(&self) -> ExchangeResult<Value> {
self.get(TwelvedataEndpoint::Cryptocurrencies, HashMap::new())
.await
}
pub async fn market_state(&self, exchange: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("exchange".to_string(), exchange.to_string());
self.get(TwelvedataEndpoint::MarketState, params).await
}
pub async fn rsi(
&self,
symbol: &Symbol,
interval: &str,
time_period: u32,
) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(symbol));
params.insert("interval".to_string(), map_interval(interval).to_string());
params.insert("time_period".to_string(), time_period.to_string());
self.get(TwelvedataEndpoint::Rsi, params).await
}
pub async fn macd(&self, symbol: &Symbol, interval: &str) -> ExchangeResult<Value> {
let mut params = HashMap::new();
params.insert("symbol".to_string(), format_symbol(symbol));
params.insert("interval".to_string(), map_interval(interval).to_string());
self.get(TwelvedataEndpoint::Macd, params).await
}
}