use std::collections::HashMap;
use crate::core::types::{ExchangeId, ExchangeType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConnectorCategory {
CryptoExchangeCex,
CryptoExchangeDex,
StockMarketUS,
StockMarketIndia,
StockMarketJapan,
StockMarketKorea,
StockMarketRussia,
Forex,
DataFeed,
Broker,
DataProvider,
}
#[derive(Debug, Clone, Copy)]
pub struct Features {
pub market_data: bool,
pub trading: bool,
pub account: bool,
pub positions: bool,
pub websocket: bool,
pub ws_klines: bool,
pub ws_trades: bool,
pub ws_orderbook: bool,
pub ws_ticker: bool,
pub cancel_all: bool,
pub amend_order: bool,
pub batch_orders: bool,
pub account_transfers: bool,
pub custodial_funds: bool,
pub sub_accounts: bool,
pub margin_trading: bool,
pub trigger_orders: bool,
pub convert_swap: bool,
pub earn_staking: bool,
pub copy_trading: bool,
}
impl Features {
pub const fn full() -> Self {
Self {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
pub const fn data_only() -> Self {
Self {
market_data: true,
trading: false,
account: false,
positions: false,
websocket: false,
ws_klines: false,
ws_trades: false,
ws_orderbook: false,
ws_ticker: false,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
pub const fn data_with_ws() -> Self {
Self {
market_data: true,
trading: false,
account: false,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
pub const fn broker() -> Self {
Self {
market_data: true,
trading: true,
account: true,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
pub const fn spot_exchange() -> Self {
Self {
market_data: true,
trading: true,
account: true,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
pub const fn dex() -> Self {
Self {
market_data: true,
trading: true,
account: false,
positions: false,
websocket: false,
ws_klines: false,
ws_trades: false,
ws_orderbook: false,
ws_ticker: false,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthType {
ApiKey,
OAuth2,
TOTP,
BasicAuth,
BearerToken,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimiterModel {
SimpleCounter,
WeightBased,
DecayingCounter,
GroupBased,
Unknown,
}
#[derive(Debug, Clone, Copy)]
pub struct RateLimitGroup {
pub name: &'static str,
pub max_value: u32,
pub window_seconds: u32,
pub is_weight: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct RateLimits {
pub requests_per_second: Option<u32>,
pub requests_per_minute: Option<u32>,
pub weight_per_minute: Option<u32>,
pub window_seconds: u32,
pub limiter_model: LimiterModel,
pub groups: &'static [RateLimitGroup],
pub has_server_headers: bool,
}
impl RateLimits {
pub const fn none() -> Self {
Self {
requests_per_second: None,
requests_per_minute: None,
weight_per_minute: None,
window_seconds: 60,
limiter_model: LimiterModel::Unknown,
groups: &[],
has_server_headers: false,
}
}
pub const fn standard(rps: u32, rpm: u32) -> Self {
Self {
requests_per_second: Some(rps),
requests_per_minute: Some(rpm),
weight_per_minute: None,
window_seconds: 60,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
}
}
pub const fn weight_based(rps: u32, rpm: u32, wpm: u32) -> Self {
Self {
requests_per_second: Some(rps),
requests_per_minute: Some(rpm),
weight_per_minute: Some(wpm),
window_seconds: 60,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ConnectorMetadata {
pub id: ExchangeId,
pub name: &'static str,
pub exchange_type: ExchangeType,
pub category: ConnectorCategory,
pub supported_features: Features,
pub authentication: AuthType,
pub rate_limits: RateLimits,
pub base_url: &'static str,
pub websocket_url: Option<&'static str>,
pub documentation_url: Option<&'static str>,
pub requires_api_key_for_data: bool,
pub requires_api_key_for_trading: bool,
pub free_tier: bool,
}
static COINBASE_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "public", max_value: 10, window_seconds: 1, is_weight: false },
RateLimitGroup { name: "private", max_value: 30, window_seconds: 1, is_weight: false },
];
static GATEIO_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "spot", max_value: 200, window_seconds: 10, is_weight: false },
RateLimitGroup { name: "futures", max_value: 200, window_seconds: 10, is_weight: false },
];
static HTX_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "spot_pub", max_value: 100, window_seconds: 10, is_weight: false },
];
static BITGET_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "market", max_value: 20, window_seconds: 1, is_weight: false },
RateLimitGroup { name: "trading", max_value: 10, window_seconds: 1, is_weight: false },
];
static BINGX_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "market", max_value: 100, window_seconds: 10, is_weight: false },
];
static UPBIT_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "market", max_value: 10, window_seconds: 1, is_weight: false },
RateLimitGroup { name: "account", max_value: 30, window_seconds: 1, is_weight: false },
RateLimitGroup { name: "order", max_value: 8, window_seconds: 1, is_weight: false },
];
static GEMINI_GROUPS: &[RateLimitGroup] = &[
RateLimitGroup { name: "public", max_value: 120, window_seconds: 60, is_weight: false },
RateLimitGroup { name: "private", max_value: 600, window_seconds: 60, is_weight: false },
];
static CONNECTOR_METADATA_ARRAY: &[ConnectorMetadata] = &[
ConnectorMetadata {
id: ExchangeId::Binance,
name: "Binance",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(10),
requests_per_minute: Some(1200),
weight_per_minute: Some(6000),
window_seconds: 60,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: true,
},
base_url: "https://api.binance.com",
websocket_url: Some("wss://stream.binance.com:9443"),
documentation_url: Some("https://binance-docs.github.io/apidocs/spot/en/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Bybit,
name: "Bybit",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(120),
weight_per_minute: None,
window_seconds: 5,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: true,
},
base_url: "https://api.bybit.com",
websocket_url: Some("wss://stream.bybit.com/v5/public/spot"),
documentation_url: Some("https://bybit-exchange.github.io/docs/v5/intro"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::OKX,
name: "OKX",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(10),
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 2,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://www.okx.com",
websocket_url: Some("wss://ws.okx.com:8443/ws/v5/public"),
documentation_url: Some("https://www.okx.com/docs-v5/en/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::KuCoin,
name: "KuCoin",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(120),
weight_per_minute: Some(4000),
window_seconds: 30,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: true,
},
base_url: "https://api.kucoin.com",
websocket_url: Some("wss://ws-api-spot.kucoin.com"),
documentation_url: Some("https://docs.kucoin.com/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Kraken,
name: "Kraken",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: false,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(1),
requests_per_minute: Some(60),
weight_per_minute: None,
window_seconds: 0,
limiter_model: LimiterModel::DecayingCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://api.kraken.com",
websocket_url: Some("wss://ws.kraken.com"),
documentation_url: Some("https://docs.kraken.com/rest/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Coinbase,
name: "Coinbase",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(10),
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 1,
limiter_model: LimiterModel::SimpleCounter,
groups: COINBASE_GROUPS,
has_server_headers: true,
},
base_url: "https://api.coinbase.com",
websocket_url: Some("wss://ws-feed.exchange.coinbase.com"),
documentation_url: Some("https://docs.cdp.coinbase.com/exchange/docs/welcome/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::GateIO,
name: "Gate.io",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(1200),
weight_per_minute: None,
window_seconds: 10,
limiter_model: LimiterModel::SimpleCounter,
groups: GATEIO_GROUPS,
has_server_headers: true,
},
base_url: "https://api.gateio.ws",
websocket_url: Some("wss://api.gateio.ws/ws/v4/"),
documentation_url: Some("https://www.gate.io/docs/developers/apiv4/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Bitfinex,
name: "Bitfinex",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(90),
weight_per_minute: None,
window_seconds: 60,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://api-pub.bitfinex.com",
websocket_url: Some("wss://api-pub.bitfinex.com/ws/2"),
documentation_url: Some("https://docs.bitfinex.com/docs"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Bitstamp,
name: "Bitstamp",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: false,
websocket: true,
ws_klines: false,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: false,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(10),
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 1,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://www.bitstamp.net",
websocket_url: Some("wss://ws.bitstamp.net"),
documentation_url: Some("https://www.bitstamp.net/api/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Gemini,
name: "Gemini",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(120),
weight_per_minute: None,
window_seconds: 60,
limiter_model: LimiterModel::SimpleCounter,
groups: GEMINI_GROUPS,
has_server_headers: false,
},
base_url: "https://api.gemini.com",
websocket_url: Some("wss://api.gemini.com/v1/marketdata"),
documentation_url: Some("https://docs.gemini.com/rest-api/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::MEXC,
name: "MEXC",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: false,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(1200),
weight_per_minute: Some(7200),
window_seconds: 10,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: true,
},
base_url: "https://api.mexc.com",
websocket_url: Some("wss://wbs.mexc.com/ws"),
documentation_url: Some("https://mexcdevelop.github.io/apidocs/spot_v3_en/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::HTX,
name: "HTX (Huobi)",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: false,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 10,
limiter_model: LimiterModel::SimpleCounter,
groups: HTX_GROUPS,
has_server_headers: true,
},
base_url: "https://api.huobi.pro",
websocket_url: Some("wss://api.huobi.pro/ws"),
documentation_url: Some("https://www.htx.com/en-us/opend/newApiPages/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Bitget,
name: "Bitget",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(20),
requests_per_minute: Some(1200),
weight_per_minute: None,
window_seconds: 1,
limiter_model: LimiterModel::SimpleCounter,
groups: BITGET_GROUPS,
has_server_headers: true,
},
base_url: "https://api.bitget.com",
websocket_url: Some("wss://ws.bitget.com/v2/ws/public"),
documentation_url: Some("https://www.bitget.com/api-doc/common/intro"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::BingX,
name: "BingX",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 10,
limiter_model: LimiterModel::SimpleCounter,
groups: BINGX_GROUPS,
has_server_headers: false,
},
base_url: "https://open-api.bingx.com",
websocket_url: Some("wss://open-api-ws.bingx.com/market"),
documentation_url: Some("https://bingx-api.github.io/docs/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::CryptoCom,
name: "Crypto.com",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: false,
custodial_funds: true,
sub_accounts: true,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(3),
requests_per_minute: Some(180),
weight_per_minute: None,
window_seconds: 1,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://api.crypto.com",
websocket_url: Some("wss://stream.crypto.com/exchange/v1/market"),
documentation_url: Some("https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Upbit,
name: "Upbit",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: false,
websocket: true,
ws_klines: false,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: false,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(10),
requests_per_minute: Some(600),
weight_per_minute: None,
window_seconds: 1,
limiter_model: LimiterModel::GroupBased,
groups: UPBIT_GROUPS,
has_server_headers: true,
},
base_url: "https://api.upbit.com",
websocket_url: Some("wss://api.upbit.com/websocket/v1"),
documentation_url: Some("https://docs.upbit.com/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Deribit,
name: "Deribit",
exchange_type: ExchangeType::Cex,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: false,
account_transfers: false,
custodial_funds: true,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(20),
requests_per_minute: Some(1000),
weight_per_minute: None,
window_seconds: 0,
limiter_model: LimiterModel::DecayingCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://www.deribit.com",
websocket_url: Some("wss://www.deribit.com/ws/api/v2"),
documentation_url: Some("https://docs.deribit.com/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::HyperLiquid,
name: "HyperLiquid",
exchange_type: ExchangeType::Hybrid,
category: ConnectorCategory::CryptoExchangeCex,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: true,
amend_order: true,
batch_orders: true,
account_transfers: true,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: Some(20),
requests_per_minute: Some(1200),
weight_per_minute: Some(1200),
window_seconds: 60,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: false,
},
base_url: "https://api.hyperliquid.xyz",
websocket_url: Some("wss://api.hyperliquid.xyz/ws"),
documentation_url: Some("https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Lighter,
name: "Lighter",
exchange_type: ExchangeType::Dex,
category: ConnectorCategory::CryptoExchangeDex,
supported_features: Features {
market_data: true,
trading: true,
account: false,
positions: false,
websocket: true,
ws_klines: false,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::None,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: None,
weight_per_minute: Some(10_000),
window_seconds: 60,
limiter_model: LimiterModel::WeightBased,
groups: &[],
has_server_headers: false,
},
base_url: "https://api.lighter.xyz",
websocket_url: Some("wss://mainnet.zklighter.elliot.ai/stream"),
documentation_url: Some("https://docs.lighter.xyz/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Dydx,
name: "dYdX",
exchange_type: ExchangeType::Dex,
category: ConnectorCategory::CryptoExchangeDex,
supported_features: Features {
market_data: true,
trading: true,
account: false,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey,
rate_limits: RateLimits {
requests_per_second: None,
requests_per_minute: Some(360),
weight_per_minute: None,
window_seconds: 10,
limiter_model: LimiterModel::SimpleCounter,
groups: &[],
has_server_headers: false,
},
base_url: "https://api.dydx.exchange",
websocket_url: Some("wss://api.dydx.exchange/v3/ws"),
documentation_url: Some("https://docs.dydx.exchange/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Polygon,
name: "Polygon.io",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketUS,
supported_features: Features::data_with_ws(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(5, 5),
base_url: "https://api.polygon.io",
websocket_url: Some("wss://socket.polygon.io"),
documentation_url: Some("https://polygon.io/docs/stocks"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Finnhub,
name: "Finnhub",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketUS,
supported_features: Features::data_with_ws(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(1, 60),
base_url: "https://finnhub.io",
websocket_url: Some("wss://ws.finnhub.io"),
documentation_url: Some("https://finnhub.io/docs/api"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Tiingo,
name: "Tiingo",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketUS,
supported_features: Features::data_with_ws(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(50, 1000),
base_url: "https://api.tiingo.com",
websocket_url: Some("wss://api.tiingo.com/iex"),
documentation_url: Some("https://www.tiingo.com/documentation/general/overview"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Twelvedata,
name: "Twelve Data",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketUS,
supported_features: Features::data_with_ws(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(8, 800),
base_url: "https://api.twelvedata.com",
websocket_url: Some("wss://ws.twelvedata.com"),
documentation_url: Some("https://twelvedata.com/docs"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Alpaca,
name: "Alpaca",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketUS,
supported_features: Features::broker(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(3, 200),
base_url: "https://api.alpaca.markets",
websocket_url: Some("wss://stream.data.alpaca.markets/v2"),
documentation_url: Some("https://docs.alpaca.markets/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::AngelOne,
name: "Angel One",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketIndia,
supported_features: Features::broker(),
authentication: AuthType::TOTP,
rate_limits: RateLimits::standard(10, 600),
base_url: "https://apiconnect.angelbroking.com",
websocket_url: Some("wss://smartapisocket.angelone.in/smart-stream"),
documentation_url: Some("https://smartapi.angelbroking.com/docs"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Zerodha,
name: "Zerodha (Kite)",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketIndia,
supported_features: Features::broker(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(3, 180),
base_url: "https://api.kite.trade",
websocket_url: Some("wss://ws.kite.trade"),
documentation_url: Some("https://kite.trade/docs/connect/v3/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: false,
},
ConnectorMetadata {
id: ExchangeId::Upstox,
name: "Upstox",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketIndia,
supported_features: Features::broker(),
authentication: AuthType::OAuth2,
rate_limits: RateLimits::standard(25, 1000),
base_url: "https://api.upstox.com",
websocket_url: Some("wss://api.upstox.com/v2/feed/market-data-feed"),
documentation_url: Some("https://upstox.com/developer/api-documentation/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: false,
},
ConnectorMetadata {
id: ExchangeId::Dhan,
name: "Dhan",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketIndia,
supported_features: Features::broker(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(10, 600),
base_url: "https://api.dhan.co",
websocket_url: Some("wss://api-feed.dhan.co"),
documentation_url: Some("https://dhanhq.co/docs/v2/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Fyers,
name: "Fyers",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketIndia,
supported_features: Features::broker(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(10, 600),
base_url: "https://api-t1.fyers.in",
websocket_url: Some("wss://api-t1.fyers.in/socket/v2"),
documentation_url: Some("https://fyers.in/api-documentation/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::JQuants,
name: "J-Quants",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketJapan,
supported_features: Features::data_only(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(10, 600),
base_url: "https://api.jquants.com",
websocket_url: None,
documentation_url: Some("https://jpx.gitbook.io/j-quants-en"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Krx,
name: "Korea Exchange (KRX)",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketKorea,
supported_features: Features::data_only(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(5, 100),
base_url: "https://data.krx.co.kr",
websocket_url: None,
documentation_url: Some("https://data.krx.co.kr/contents/MDC/MAIN/main/index.cmd"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Moex,
name: "Moscow Exchange (MOEX)",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::StockMarketRussia,
supported_features: Features {
market_data: true,
trading: false,
account: false,
positions: false,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::None,
rate_limits: RateLimits::standard(10, 600),
base_url: "https://iss.moex.com",
websocket_url: Some("wss://iss.moex.com/infocx/v3/websocket"),
documentation_url: Some("https://www.moex.com/a2193"),
requires_api_key_for_data: false,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Tinkoff,
name: "Tinkoff Invest",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::StockMarketRussia,
supported_features: Features::broker(),
authentication: AuthType::BearerToken,
rate_limits: RateLimits::standard(100, 300),
base_url: "https://invest-public-api.tinkoff.ru",
websocket_url: Some("wss://invest-public-api.tinkoff.ru/ws"),
documentation_url: Some("https://tinkoff.github.io/investAPI/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Oanda,
name: "OANDA",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::Forex,
supported_features: Features::broker(),
authentication: AuthType::BearerToken,
rate_limits: RateLimits::standard(10, 120),
base_url: "https://api-fxtrade.oanda.com",
websocket_url: Some("wss://stream-fxtrade.oanda.com"),
documentation_url: Some("https://developer.oanda.com/rest-live-v20/introduction/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Dukascopy,
name: "Dukascopy",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::Forex,
supported_features: Features::data_only(),
authentication: AuthType::None,
rate_limits: RateLimits::standard(5, 300),
base_url: "https://datafeed.dukascopy.com",
websocket_url: None,
documentation_url: Some("https://www.dukascopy.com/swiss/english/marketwatch/historical/"),
requires_api_key_for_data: false,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::AlphaVantage,
name: "Alpha Vantage",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::Forex,
supported_features: Features::data_only(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(1, 5),
base_url: "https://www.alphavantage.co",
websocket_url: None,
documentation_url: Some("https://www.alphavantage.co/documentation/"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Polymarket,
name: "Polymarket",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::DataFeed,
supported_features: Features {
market_data: true,
trading: false,
account: false,
positions: false,
websocket: true,
ws_klines: false,
ws_trades: true,
ws_orderbook: true,
ws_ticker: false,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::ApiKey, rate_limits: RateLimits::standard(10, 500), base_url: "https://clob.polymarket.com",
websocket_url: Some("wss://ws-subscriptions-clob.polymarket.com/ws/market"),
documentation_url: Some("https://docs.polymarket.com/"),
requires_api_key_for_data: false, requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::Ib,
name: "Interactive Brokers",
exchange_type: ExchangeType::Broker,
category: ConnectorCategory::Broker,
supported_features: Features {
market_data: true,
trading: true,
account: true,
positions: true,
websocket: true,
ws_klines: true,
ws_trades: true,
ws_orderbook: true,
ws_ticker: true,
cancel_all: false,
amend_order: false,
batch_orders: false,
account_transfers: false,
custodial_funds: false,
sub_accounts: false,
margin_trading: false,
trigger_orders: false,
convert_swap: false,
earn_staking: false,
copy_trading: false,
},
authentication: AuthType::OAuth2,
rate_limits: RateLimits::none(),
base_url: "https://localhost:5000/v1/api", websocket_url: Some("wss://localhost:5000/v1/api/ws"),
documentation_url: Some("https://www.interactivebrokers.com/api/doc.html"),
requires_api_key_for_data: true,
requires_api_key_for_trading: true,
free_tier: false,
},
ConnectorMetadata {
id: ExchangeId::YahooFinance,
name: "Yahoo Finance",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::DataProvider,
supported_features: Features::data_only(),
authentication: AuthType::None,
rate_limits: RateLimits::standard(5, 100),
base_url: "https://query1.finance.yahoo.com",
websocket_url: None,
documentation_url: None,
requires_api_key_for_data: false,
requires_api_key_for_trading: false,
free_tier: true,
},
ConnectorMetadata {
id: ExchangeId::CryptoCompare,
name: "CryptoCompare",
exchange_type: ExchangeType::DataProvider,
category: ConnectorCategory::DataProvider,
supported_features: Features::data_with_ws(),
authentication: AuthType::ApiKey,
rate_limits: RateLimits::standard(10, 250000),
base_url: "https://min-api.cryptocompare.com",
websocket_url: Some("wss://streamer.cryptocompare.com/v2"),
documentation_url: Some("https://min-api.cryptocompare.com/documentation"),
requires_api_key_for_data: true,
requires_api_key_for_trading: false,
free_tier: true,
},
];
pub struct ConnectorRegistry {
lookup: HashMap<ExchangeId, &'static ConnectorMetadata>,
}
impl ConnectorRegistry {
pub fn new() -> Self {
let lookup = CONNECTOR_METADATA_ARRAY
.iter()
.map(|meta| (meta.id, meta))
.collect();
Self { lookup }
}
pub fn get(&self, id: &ExchangeId) -> Option<&'static ConnectorMetadata> {
self.lookup.get(id).copied()
}
pub fn iter(&self) -> impl Iterator<Item = &'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter()
}
pub fn list_all(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().collect()
}
pub fn list_by_category(&self, category: ConnectorCategory) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY
.iter()
.filter(|m| m.category == category)
.collect()
}
pub fn list_by_type(&self, exchange_type: ExchangeType) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY
.iter()
.filter(|m| m.exchange_type == exchange_type)
.collect()
}
pub fn list_with_trading(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY
.iter()
.filter(|m| m.supported_features.trading)
.collect()
}
pub fn list_with_websocket(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY
.iter()
.filter(|m| m.supported_features.websocket)
.collect()
}
pub fn count(&self) -> usize {
CONNECTOR_METADATA_ARRAY.len()
}
}
impl Default for ConnectorRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_default() {
let registry = ConnectorRegistry::default();
assert_eq!(registry.count(), CONNECTOR_METADATA_ARRAY.len());
assert!(registry.count() > 0, "Default registry should not be empty");
}
#[test]
fn test_registry_new() {
let registry = ConnectorRegistry::new();
assert!(registry.count() > 0);
assert_eq!(registry.count(), CONNECTOR_METADATA_ARRAY.len());
}
#[test]
fn test_registry_count() {
let registry = ConnectorRegistry::new();
assert_eq!(registry.count(), CONNECTOR_METADATA_ARRAY.len());
assert_eq!(registry.count(), 47, "Should have exactly 47 active connectors");
let all = registry.list_all();
assert_eq!(all.len(), 47, "list_all() should return 47 entries");
}
#[test]
fn test_registry_get_binance() {
let registry = ConnectorRegistry::new();
let binance = registry.get(&ExchangeId::Binance);
assert!(binance.is_some(), "Binance should be in registry");
let meta = binance.unwrap();
assert_eq!(meta.id, ExchangeId::Binance);
assert_eq!(meta.name, "Binance");
assert_eq!(meta.exchange_type, ExchangeType::Cex);
assert_eq!(meta.category, ConnectorCategory::CryptoExchangeCex);
assert!(meta.supported_features.market_data);
assert!(meta.supported_features.trading);
assert_eq!(meta.base_url, "https://api.binance.com");
}
#[test]
fn test_registry_get_okx() {
let registry = ConnectorRegistry::new();
let okx = registry.get(&ExchangeId::OKX);
assert!(okx.is_some(), "OKX should be in registry");
let meta = okx.unwrap();
assert_eq!(meta.id, ExchangeId::OKX);
assert_eq!(meta.name, "OKX");
assert_eq!(meta.exchange_type, ExchangeType::Cex);
assert_eq!(meta.category, ConnectorCategory::CryptoExchangeCex);
assert!(meta.supported_features.websocket);
}
#[test]
fn test_registry_get_alpaca() {
let registry = ConnectorRegistry::new();
let alpaca = registry.get(&ExchangeId::Alpaca);
assert!(alpaca.is_some(), "Alpaca should be in registry");
let meta = alpaca.unwrap();
assert_eq!(meta.id, ExchangeId::Alpaca);
assert_eq!(meta.name, "Alpaca");
assert_eq!(meta.exchange_type, ExchangeType::Broker);
assert_eq!(meta.category, ConnectorCategory::StockMarketUS);
assert!(meta.supported_features.trading, "Alpaca is a broker with trading");
assert!(meta.requires_api_key_for_data);
assert!(meta.requires_api_key_for_trading);
}
#[test]
fn test_registry_get_missing() {
let registry = ConnectorRegistry::new();
let missing = registry.get(&ExchangeId::Custom(999));
assert!(missing.is_none(), "Invalid ExchangeId should return None");
}
#[test]
fn test_registry_iter() {
let registry = ConnectorRegistry::new();
let count = registry.iter().count();
assert_eq!(count, 43, "iter() should return all 43 connectors");
let all: Vec<_> = registry.iter().collect();
assert_eq!(all.len(), 43);
}
#[test]
fn test_registry_list_by_category_cex() {
let registry = ConnectorRegistry::new();
let cex = registry.list_by_category(ConnectorCategory::CryptoExchangeCex);
assert_eq!(cex.len(), 19, "Should have exactly 19 CEX connectors");
for meta in &cex {
assert!(
meta.exchange_type == ExchangeType::Cex || meta.exchange_type == ExchangeType::Hybrid,
"{} should be CEX or Hybrid type",
meta.name
);
}
}
#[test]
fn test_registry_list_by_category_dex() {
let registry = ConnectorRegistry::new();
let dex = registry.list_by_category(ConnectorCategory::CryptoExchangeDex);
assert_eq!(dex.len(), 3, "Should have exactly 3 DEX connectors");
for meta in &dex {
assert_eq!(meta.exchange_type, ExchangeType::Dex, "{} should be DEX type", meta.name);
}
}
#[test]
fn test_registry_list_by_category_stock_us() {
let registry = ConnectorRegistry::new();
let stocks = registry.list_by_category(ConnectorCategory::StockMarketUS);
assert_eq!(stocks.len(), 5, "Should have exactly 5 US stock connectors");
let names: Vec<&str> = stocks.iter().map(|m| m.name).collect();
assert!(names.contains(&"Polygon.io"));
assert!(names.contains(&"Finnhub"));
assert!(names.contains(&"Alpaca"));
}
#[test]
fn test_registry_list_by_type_cex() {
let registry = ConnectorRegistry::new();
let cex_type = registry.list_by_type(ExchangeType::Cex);
assert!(cex_type.len() >= 17, "Should have at least 17 CEX-type connectors");
for meta in &cex_type {
assert_eq!(meta.exchange_type, ExchangeType::Cex);
}
}
#[test]
fn test_registry_list_with_trading() {
let registry = ConnectorRegistry::new();
let trading = registry.list_with_trading();
assert!(trading.len() >= 20, "Should have at least 20 connectors with trading");
for meta in &trading {
assert!(
meta.supported_features.trading,
"{} should have trading feature enabled",
meta.name
);
}
}
#[test]
fn test_registry_list_with_websocket() {
let registry = ConnectorRegistry::new();
let websocket = registry.list_with_websocket();
assert!(websocket.len() >= 15, "Should have at least 15 connectors with WebSocket");
for meta in &websocket {
assert!(
meta.supported_features.websocket,
"{} should have websocket feature enabled",
meta.name
);
}
}
#[test]
fn test_registry_metadata_fields() {
let registry = ConnectorRegistry::new();
for meta in registry.iter() {
assert!(!meta.name.is_empty(), "Connector should have non-empty name");
assert!(!meta.base_url.is_empty(), "Connector {} should have non-empty base_url", meta.name);
if meta.supported_features.websocket {
assert!(
meta.websocket_url.is_some(),
"Connector {} has websocket feature but no websocket_url",
meta.name
);
}
}
}
#[test]
fn test_registry_all_categories_covered() {
let registry = ConnectorRegistry::new();
use std::collections::HashSet;
let categories: HashSet<_> = registry.iter().map(|m| m.category).collect();
assert!(categories.contains(&ConnectorCategory::CryptoExchangeCex));
assert!(categories.contains(&ConnectorCategory::CryptoExchangeDex));
assert!(categories.contains(&ConnectorCategory::StockMarketUS));
assert!(categories.contains(&ConnectorCategory::StockMarketIndia));
assert!(categories.contains(&ConnectorCategory::StockMarketJapan));
assert!(categories.contains(&ConnectorCategory::StockMarketKorea));
assert!(categories.contains(&ConnectorCategory::StockMarketRussia));
assert!(categories.contains(&ConnectorCategory::Forex));
assert!(categories.contains(&ConnectorCategory::DataFeed));
assert!(categories.contains(&ConnectorCategory::Broker));
assert!(categories.contains(&ConnectorCategory::DataProvider));
assert_eq!(categories.len(), 11, "Should use all 11 ConnectorCategory variants");
}
#[test]
fn test_registry_no_duplicates() {
use std::collections::HashSet;
let ids: HashSet<ExchangeId> = CONNECTOR_METADATA_ARRAY.iter().map(|m| m.id).collect();
assert_eq!(
ids.len(),
CONNECTOR_METADATA_ARRAY.len(),
"Duplicate ExchangeId found in registry"
);
}
#[test]
fn test_features_full() {
let features = Features::full();
assert!(features.market_data);
assert!(features.trading);
assert!(features.account);
assert!(features.positions);
assert!(features.websocket);
}
#[test]
fn test_features_data_only() {
let features = Features::data_only();
assert!(features.market_data);
assert!(!features.trading);
assert!(!features.account);
assert!(!features.positions);
assert!(!features.websocket);
}
#[test]
fn test_features_dex() {
let features = Features::dex();
assert!(features.market_data);
assert!(features.trading);
assert!(!features.account);
assert!(!features.positions);
assert!(!features.websocket);
}
#[test]
fn test_rate_limits_none() {
let limits = RateLimits::none();
assert!(limits.requests_per_second.is_none());
assert!(limits.requests_per_minute.is_none());
assert!(limits.weight_per_minute.is_none());
}
#[test]
fn test_rate_limits_standard() {
let limits = RateLimits::standard(10, 600);
assert_eq!(limits.requests_per_second, Some(10));
assert_eq!(limits.requests_per_minute, Some(600));
assert!(limits.weight_per_minute.is_none());
}
#[test]
fn test_registry_o1_lookup() {
let registry = ConnectorRegistry::new();
for _ in 0..100 {
let _ = registry.get(&ExchangeId::Binance);
let _ = registry.get(&ExchangeId::OKX);
let _ = registry.get(&ExchangeId::Lighter);
}
}
#[test]
fn test_static_metadata_no_heap() {
assert_eq!(CONNECTOR_METADATA_ARRAY.len(), 47);
let first = &CONNECTOR_METADATA_ARRAY[0];
assert!(!first.name.is_empty());
}
#[test]
fn test_registry_complex_filtering() {
let registry = ConnectorRegistry::new();
let cex_ws_trading: Vec<_> = registry
.list_by_category(ConnectorCategory::CryptoExchangeCex)
.into_iter()
.filter(|m| m.supported_features.websocket && m.supported_features.trading)
.collect();
assert!(cex_ws_trading.len() >= 10, "Should have at least 10 CEX with WS + trading");
}
#[test]
fn test_registry_free_tier() {
let registry = ConnectorRegistry::new();
let free_connectors: Vec<_> = registry.iter().filter(|m| m.free_tier).collect();
assert!(free_connectors.len() >= 35, "Should have at least 35 connectors with free tier");
let binance = registry.get(&ExchangeId::Binance).unwrap();
assert!(binance.free_tier, "Binance should have free tier");
let lighter = registry.get(&ExchangeId::Lighter).unwrap();
assert!(lighter.free_tier, "Lighter should have free tier");
}
#[test]
fn test_auth_types() {
let registry = ConnectorRegistry::new();
let api_key_count = registry.iter().filter(|m| m.authentication == AuthType::ApiKey).count();
let none_count = registry.iter().filter(|m| m.authentication == AuthType::None).count();
let oauth_count = registry.iter().filter(|m| m.authentication == AuthType::OAuth2).count();
assert!(api_key_count >= 30, "Should have at least 30 connectors with API key auth");
assert!(none_count >= 5, "Should have at least 5 connectors with no auth");
assert!(oauth_count >= 1, "Should have at least 1 connector with OAuth2");
}
}