use crate::core::types::{
ExchangeId, ExchangeType,
RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, DecayingLimitConfig,
};
#[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)]
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: RateLimitCapabilities,
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_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "public", max_budget: 10, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("X-RateLimit-Remaining"), header_reports_used: false },
RestLimitPool { name: "private", max_budget: 30, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("X-RateLimit-Remaining"), header_reports_used: false },
];
static GATEIO_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "spot", max_budget: 200, window_seconds: 10, is_weight: false, has_server_headers: true, server_header: Some("X-Gate-RateLimit-Remaining"), header_reports_used: false },
RestLimitPool { name: "futures", max_budget: 200, window_seconds: 10, is_weight: false, has_server_headers: true, server_header: Some("X-Gate-RateLimit-Remaining"), header_reports_used: false },
];
static HTX_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "spot_pub", max_budget: 100, window_seconds: 10, is_weight: true, has_server_headers: true, server_header: Some("X-RateLimit-Used"), header_reports_used: true },
];
static BITGET_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "market", max_budget: 20, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("X-Bapi-Limit"), header_reports_used: true },
RestLimitPool { name: "trading", max_budget: 10, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("X-Bapi-Limit"), header_reports_used: true },
];
static UPBIT_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "market", max_budget: 10, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("Remaining-Req"), header_reports_used: false },
RestLimitPool { name: "account", max_budget: 30, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("Remaining-Req"), header_reports_used: false },
RestLimitPool { name: "order", max_budget: 8, window_seconds: 1, is_weight: false, has_server_headers: true, server_header: Some("Remaining-Req"), header_reports_used: false },
];
static GEMINI_POOLS: &[RestLimitPool] = &[
RestLimitPool { name: "public", max_budget: 120, window_seconds: 60, is_weight: false, has_server_headers: false, server_header: None, header_reports_used: false },
RestLimitPool { name: "private", max_budget: 600, window_seconds: 60, is_weight: false, has_server_headers: false, server_header: None, header_reports_used: 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: RateLimitCapabilities {
model: LimitModel::Weight,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 6000,
window_seconds: 60,
is_weight: true,
has_server_headers: true,
server_header: Some("X-MBX-USED-WEIGHT-1M"),
header_reports_used: true,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: None,
max_msg_per_sec: Some(5),
max_streams_per_conn: Some(1024),
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 5,
is_weight: false,
has_server_headers: true,
server_header: Some("X-Bapi-Limit-Status"),
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 40,
window_seconds: 2,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Weight,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 4000,
window_seconds: 30,
is_weight: true,
has_server_headers: true,
server_header: Some("X-RateLimit-Used"),
header_reports_used: true,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: Some(800),
max_subs_per_conn: Some(300),
max_msg_per_sec: Some(10),
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Decaying,
rest_pools: &[],
decaying: Some(DecayingLimitConfig {
max_counter: 15.0,
decay_rate_per_sec: 0.33,
default_cost: 1.0,
}),
endpoint_weights: &[],
ws: WsLimits {
max_connections: Some(150),
max_subs_per_conn: None,
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: COINBASE_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: None,
max_msg_per_sec: Some(8),
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: GATEIO_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 90,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(30),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 400,
window_seconds: 1,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: GEMINI_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Weight,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 500,
window_seconds: 10,
is_weight: true,
has_server_headers: true,
server_header: Some("X-MBX-USED-WEIGHT"),
header_reports_used: true,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(30),
max_msg_per_sec: Some(100),
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: HTX_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: BITGET_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(1000),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 500,
window_seconds: 10,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(200),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 100,
window_seconds: 1,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(400),
max_msg_per_sec: Some(100),
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Group,
rest_pools: UPBIT_POOLS,
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: None,
max_msg_per_sec: Some(5),
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Decaying,
rest_pools: &[],
decaying: Some(DecayingLimitConfig {
max_counter: 50000.0,
decay_rate_per_sec: 10000.0,
default_cost: 500.0,
}),
endpoint_weights: &[],
ws: WsLimits {
max_connections: Some(32),
max_subs_per_conn: None,
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Weight,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 1200,
window_seconds: 60,
is_weight: true,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: Some(10),
max_subs_per_conn: Some(1000),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Weight,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 60,
window_seconds: 60,
is_weight: true,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: Some(100),
max_subs_per_conn: Some(100),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 100,
window_seconds: 10,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits {
max_connections: None,
max_subs_per_conn: Some(32),
max_msg_per_sec: None,
max_streams_per_conn: None,
},
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 5,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 60,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 1000,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 800,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 200,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 180,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 1000,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 100,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 600,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 300,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 120,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 300,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 5,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 500,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities::unlimited(),
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 100,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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: RateLimitCapabilities {
model: LimitModel::Simple,
rest_pools: &[RestLimitPool {
name: "default",
max_budget: 250000,
window_seconds: 60,
is_weight: false,
has_server_headers: false,
server_header: None,
header_reports_used: false,
}],
decaying: None,
endpoint_weights: &[],
ws: WsLimits::unlimited(),
},
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(crate) struct ConnectorRegistry;
impl ConnectorRegistry {
pub(crate) fn new() -> Self {
Self
}
pub(crate) fn list_all(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().collect()
}
}
#[cfg(test)]
impl ConnectorRegistry {
pub(crate) fn get(&self, id: &ExchangeId) -> Option<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().find(|m| m.id == *id)
}
fn iter(&self) -> impl Iterator<Item = &'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter()
}
fn count(&self) -> usize {
CONNECTOR_METADATA_ARRAY.len()
}
fn list_by_category(&self, category: ConnectorCategory) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().filter(|m| m.category == category).collect()
}
fn list_by_type(&self, exchange_type: ExchangeType) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().filter(|m| m.exchange_type == exchange_type).collect()
}
fn list_with_trading(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().filter(|m| m.supported_features.trading).collect()
}
fn list_with_websocket(&self) -> Vec<&'static ConnectorMetadata> {
CONNECTOR_METADATA_ARRAY.iter().filter(|m| m.supported_features.websocket).collect()
}
}
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();
let n = CONNECTOR_METADATA_ARRAY.len();
assert!(n >= 30, "registry shrunk below 30 — accidental wipe?");
assert_eq!(registry.count(), n);
assert_eq!(registry.list_all().len(), n);
}
#[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 n = CONNECTOR_METADATA_ARRAY.len();
assert_eq!(registry.iter().count(), n);
let all: Vec<_> = registry.iter().collect();
assert_eq!(all.len(), n);
}
#[test]
fn test_registry_list_by_category_cex() {
let registry = ConnectorRegistry::new();
let cex = registry.list_by_category(ConnectorCategory::CryptoExchangeCex);
assert!(cex.len() >= 17, "expected at least 17 CEX/Hybrid connectors, got {}", cex.len());
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(), 2, "Should have exactly 2 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_limit_capabilities_unlimited() {
let limits = RateLimitCapabilities::unlimited();
assert_eq!(limits.model, LimitModel::Unlimited);
assert!(limits.rest_pools.is_empty());
assert!(limits.decaying.is_none());
assert!(limits.endpoint_weights.is_empty());
assert!(limits.ws.max_connections.is_none());
assert!(limits.ws.max_subs_per_conn.is_none());
assert!(limits.ws.max_msg_per_sec.is_none());
assert!(limits.ws.max_streams_per_conn.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!(
!CONNECTOR_METADATA_ARRAY.is_empty(),
"CONNECTOR_METADATA_ARRAY is empty — every connector was unregistered"
);
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 >= 2, "Should have at least 2 connectors with no auth");
assert!(oauth_count >= 1, "Should have at least 1 connector with OAuth2");
}
}