use crate::{ReqwestHttpTransport, TungsteniteWsTransport};
use wickra_exchange_core::{
AdvancedOrders, Backoff, Binance, Bitget, Bybit, Coinbase, Credentials, Derivatives, Error,
Exchange, ExchangeOptions, Gate, HttpTransport, Htx, Kraken, KuCoin, MarketType, Okx, Result,
ThrottledTransport, Upbit, WsExecution, WsTransport, WsUserData,
};
const RETRY_POLICY: Backoff = Backoff::new(250, 8_000, 3);
fn ensure_market_supported(name: &str, market: MarketType) -> Result<()> {
let supported = match market {
MarketType::Spot => true,
MarketType::UsdMFutures => !matches!(name, "coinbase" | "upbit"),
MarketType::CoinMFutures => matches!(name, "bybit" | "okx"),
MarketType::Margin => false,
};
if supported {
return Ok(());
}
Err(Error::Exchange {
code: "unsupported".to_string(),
message: format!(
"{name}: {market:?} is not routed by this client; it would fall \
through to a different market's endpoints and trade the wrong \
instrument"
),
})
}
fn transports(options: &ExchangeOptions) -> Result<(Box<dyn HttpTransport>, Box<dyn WsTransport>)> {
let socket = Box::new(ReqwestHttpTransport::new(options)?) as Box<dyn HttpTransport>;
let http = Box::new(ThrottledTransport::new(socket, RETRY_POLICY)) as Box<dyn HttpTransport>;
let ws = Box::new(TungsteniteWsTransport::new()) as Box<dyn WsTransport>;
Ok((http, ws))
}
pub fn connect(
name: &str,
credentials: Credentials,
options: &ExchangeOptions,
) -> Result<Box<dyn Exchange>> {
ensure_market_supported(&name.to_ascii_lowercase(), options.market_type)?;
let (http, ws) = transports(options)?;
let exchange: Box<dyn Exchange> = match name.to_ascii_lowercase().as_str() {
"binance" => Box::new(Binance::with_credentials(http, options, credentials).with_ws(ws)),
"bybit" => Box::new(Bybit::with_credentials(http, options, credentials).with_ws(ws)),
"okx" => Box::new(Okx::with_credentials(http, options, credentials).with_ws(ws)),
"bitget" => Box::new(Bitget::with_credentials(http, options, credentials).with_ws(ws)),
"kucoin" => Box::new(KuCoin::with_credentials(http, options, credentials).with_ws(ws)),
"gateio" => Box::new(Gate::with_credentials(http, options, credentials).with_ws(ws)),
"htx" => Box::new(Htx::with_credentials(http, options, credentials).with_ws(ws)),
"kraken" => Box::new(Kraken::with_credentials(http, options, credentials).with_ws(ws)),
"coinbase" => Box::new(Coinbase::with_credentials(http, options, credentials).with_ws(ws)),
"upbit" => Box::new(Upbit::with_credentials(http, options, credentials).with_ws(ws)),
other => return Err(Error::UnsupportedExchange(other.to_string())),
};
Ok(exchange)
}
pub fn connect_derivatives(
name: &str,
credentials: Credentials,
options: &ExchangeOptions,
) -> Result<Box<dyn Derivatives>> {
ensure_market_supported(&name.to_ascii_lowercase(), options.market_type)?;
let (http, ws) = transports(options)?;
let client: Box<dyn Derivatives> = match name.to_ascii_lowercase().as_str() {
"binance" => Box::new(Binance::with_credentials(http, options, credentials).with_ws(ws)),
"bybit" => Box::new(Bybit::with_credentials(http, options, credentials).with_ws(ws)),
"okx" => Box::new(Okx::with_credentials(http, options, credentials).with_ws(ws)),
"bitget" => Box::new(Bitget::with_credentials(http, options, credentials).with_ws(ws)),
"kucoin" => Box::new(KuCoin::with_credentials(http, options, credentials).with_ws(ws)),
"gateio" => Box::new(Gate::with_credentials(http, options, credentials).with_ws(ws)),
"htx" => Box::new(Htx::with_credentials(http, options, credentials).with_ws(ws)),
"kraken" => Box::new(Kraken::with_credentials(http, options, credentials).with_ws(ws)),
"coinbase" | "upbit" => {
return Err(Error::UnsupportedExchange(format!(
"{name} is spot-only (no derivatives market)"
)))
}
other => return Err(Error::UnsupportedExchange(other.to_string())),
};
Ok(client)
}
pub fn connect_advanced(
name: &str,
credentials: Credentials,
options: &ExchangeOptions,
) -> Result<Box<dyn AdvancedOrders>> {
ensure_market_supported(&name.to_ascii_lowercase(), options.market_type)?;
let (http, ws) = transports(options)?;
let client: Box<dyn AdvancedOrders> = match name.to_ascii_lowercase().as_str() {
"binance" => Box::new(Binance::with_credentials(http, options, credentials).with_ws(ws)),
"bybit" => Box::new(Bybit::with_credentials(http, options, credentials).with_ws(ws)),
"okx" => Box::new(Okx::with_credentials(http, options, credentials).with_ws(ws)),
"bitget" => Box::new(Bitget::with_credentials(http, options, credentials).with_ws(ws)),
"kucoin" => Box::new(KuCoin::with_credentials(http, options, credentials).with_ws(ws)),
"gateio" => Box::new(Gate::with_credentials(http, options, credentials).with_ws(ws)),
"htx" => Box::new(Htx::with_credentials(http, options, credentials).with_ws(ws)),
"kraken" => Box::new(Kraken::with_credentials(http, options, credentials).with_ws(ws)),
"coinbase" | "upbit" => {
return Err(Error::UnsupportedExchange(format!(
"{name} has no advanced-order surface"
)))
}
other => return Err(Error::UnsupportedExchange(other.to_string())),
};
Ok(client)
}
pub fn connect_user_data(
name: &str,
credentials: Credentials,
options: &ExchangeOptions,
) -> Result<Box<dyn WsUserData>> {
ensure_market_supported(&name.to_ascii_lowercase(), options.market_type)?;
let (http, ws) = transports(options)?;
let client: Box<dyn WsUserData> = match name.to_ascii_lowercase().as_str() {
"binance" => Box::new(Binance::with_credentials(http, options, credentials).with_ws(ws)),
"bybit" => Box::new(Bybit::with_credentials(http, options, credentials).with_ws(ws)),
"okx" => Box::new(Okx::with_credentials(http, options, credentials).with_ws(ws)),
"bitget" => Box::new(Bitget::with_credentials(http, options, credentials).with_ws(ws)),
"kucoin" => Box::new(KuCoin::with_credentials(http, options, credentials).with_ws(ws)),
"gateio" => Box::new(Gate::with_credentials(http, options, credentials).with_ws(ws)),
"htx" => Box::new(Htx::with_credentials(http, options, credentials).with_ws(ws)),
"kraken" => Box::new(Kraken::with_credentials(http, options, credentials).with_ws(ws)),
"coinbase" | "upbit" => {
return Err(Error::UnsupportedExchange(format!(
"{name} has no private user-data stream"
)))
}
other => return Err(Error::UnsupportedExchange(other.to_string())),
};
Ok(client)
}
pub fn connect_ws_execution(
name: &str,
credentials: Credentials,
options: &ExchangeOptions,
) -> Result<Box<dyn WsExecution>> {
ensure_market_supported(&name.to_ascii_lowercase(), options.market_type)?;
let (http, ws) = transports(options)?;
let client: Box<dyn WsExecution> = match name.to_ascii_lowercase().as_str() {
"binance" => Box::new(Binance::with_credentials(http, options, credentials).with_ws(ws)),
"bybit" => Box::new(Bybit::with_credentials(http, options, credentials).with_ws(ws)),
"okx" => Box::new(Okx::with_credentials(http, options, credentials).with_ws(ws)),
"bitget" => Box::new(Bitget::with_credentials(http, options, credentials).with_ws(ws)),
"kucoin" => Box::new(KuCoin::with_credentials(http, options, credentials).with_ws(ws)),
"gateio" => Box::new(Gate::with_credentials(http, options, credentials).with_ws(ws)),
"htx" => Box::new(Htx::with_credentials(http, options, credentials).with_ws(ws)),
"kraken" => Box::new(Kraken::with_credentials(http, options, credentials).with_ws(ws)),
"coinbase" | "upbit" => {
return Err(Error::UnsupportedExchange(format!(
"{name} has no WebSocket order API"
)))
}
other => return Err(Error::UnsupportedExchange(other.to_string())),
};
Ok(client)
}
#[cfg(test)]
mod tests {
use super::*;
use wickra_exchange_core::MarketType;
fn opts() -> ExchangeOptions {
ExchangeOptions::mainnet(MarketType::Spot)
}
fn creds() -> Credentials {
Credentials::new("key", "secret")
}
#[test]
fn dispatches_every_known_venue() {
for name in [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken", "coinbase",
"upbit",
] {
let client = connect(name, creds(), &opts()).unwrap();
assert!(!client.name().is_empty());
}
}
#[test]
fn name_match_is_case_insensitive() {
assert!(connect("BinAnce", creds(), &opts()).is_ok());
}
#[test]
fn unknown_venue_is_rejected() {
match connect("ftx", creds(), &opts()) {
Err(Error::UnsupportedExchange(name)) => assert_eq!(name, "ftx"),
_ => panic!("unknown venue must be rejected"),
}
}
fn futures_opts() -> ExchangeOptions {
ExchangeOptions::mainnet(MarketType::UsdMFutures)
}
#[test]
fn a_market_the_client_does_not_route_is_refused() {
const COIN_M_ROUTED: [&str; 2] = ["bybit", "okx"];
const FUTURES_VENUES: [&str; 8] = [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken",
];
for name in FUTURES_VENUES {
let options = ExchangeOptions::mainnet(MarketType::CoinMFutures);
let routed = COIN_M_ROUTED.contains(&name);
assert_eq!(
connect(name, creds(), &options).is_ok(),
routed,
"{name}: coin-margined futures should be {}",
if routed { "routed" } else { "refused" }
);
}
for name in FUTURES_VENUES {
let options = ExchangeOptions::mainnet(MarketType::Margin);
assert!(
connect(name, creds(), &options).is_err(),
"{name}: margin is not routed by any client and must be refused"
);
}
}
#[test]
fn the_spot_only_venues_refuse_futures() {
for name in ["coinbase", "upbit"] {
let options = ExchangeOptions::mainnet(MarketType::UsdMFutures);
assert!(
connect(name, creds(), &options).is_err(),
"{name} is spot-only and must refuse a futures market"
);
assert!(connect(name, creds(), &opts()).is_ok());
}
}
#[test]
fn every_entry_point_carries_the_guard() {
let margin = ExchangeOptions::mainnet(MarketType::Margin);
assert!(connect("binance", creds(), &margin).is_err());
assert!(connect_derivatives("binance", creds(), &margin).is_err());
assert!(connect_advanced("binance", creds(), &margin).is_err());
assert!(connect_user_data("binance", creds(), &margin).is_err());
assert!(connect_ws_execution("binance", creds(), &margin).is_err());
}
#[test]
fn derivatives_dispatch_covers_the_eight_futures_venues() {
for name in [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken",
] {
assert!(
connect_derivatives(name, creds(), &futures_opts()).is_ok(),
"{name} should dispatch a derivatives client"
);
}
}
#[test]
fn derivatives_rejects_spot_only_and_unknown() {
for name in ["coinbase", "upbit", "ftx"] {
match connect_derivatives(name, creds(), &opts()) {
Err(Error::UnsupportedExchange(_)) => {}
_ => panic!("{name} must be rejected for derivatives"),
}
}
}
#[test]
fn advanced_dispatch_covers_the_eight_trading_venues() {
for name in [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken",
] {
assert!(
connect_advanced(name, creds(), &opts()).is_ok(),
"{name} should dispatch an advanced-orders client"
);
}
}
#[test]
fn advanced_rejects_spot_only_and_unknown() {
for name in ["coinbase", "upbit", "ftx"] {
match connect_advanced(name, creds(), &opts()) {
Err(Error::UnsupportedExchange(_)) => {}
_ => panic!("{name} must be rejected for advanced orders"),
}
}
}
#[test]
fn user_data_dispatch_covers_the_eight_trading_venues() {
for name in [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken",
] {
let mut client = connect_user_data(name, creds(), &opts())
.unwrap_or_else(|_| panic!("{name} should dispatch a user-data client"));
assert!(client.poll_events().is_empty());
}
}
#[test]
fn user_data_rejects_spot_only_and_unknown() {
for name in ["coinbase", "upbit", "ftx"] {
match connect_user_data(name, creds(), &opts()) {
Err(Error::UnsupportedExchange(_)) => {}
_ => panic!("{name} must be rejected for user-data streaming"),
}
}
}
#[test]
fn ws_execution_dispatch_covers_the_eight_trading_venues() {
for name in [
"binance", "bybit", "okx", "bitget", "kucoin", "gateio", "htx", "kraken",
] {
assert!(
connect_ws_execution(name, creds(), &opts()).is_ok(),
"{name} should dispatch a ws-execution client"
);
}
}
#[test]
fn ws_execution_rejects_spot_only_and_unknown() {
for name in ["coinbase", "upbit", "ftx"] {
match connect_ws_execution(name, creds(), &opts()) {
Err(Error::UnsupportedExchange(_)) => {}
_ => panic!("{name} must be rejected for ws execution"),
}
}
}
}