crypto_exc_all 0.1.0

Unified cryptocurrency exchange SDK facade
Documentation
use crate::account::Balance;
use crate::config::BinanceExchangeConfig;
use crate::error::{Error, Result};
use crate::exchange::ExchangeId;
use crate::instrument::Instrument;
use crate::market::Ticker;
use binance_rs::config::{Config as BinanceConfig, Credentials as BinanceCredentials};
use binance_rs::{BinanceAccount, BinanceClient, BinanceMarket};
use serde_json::Value;

pub(crate) struct BinanceAdapter {
    account: BinanceAccount,
    market: BinanceMarket,
}

impl BinanceAdapter {
    pub(crate) fn new(config: BinanceExchangeConfig) -> Result<Self> {
        let mut binance_config = BinanceConfig::from_env();
        if let Some(api_url) = config.api_url {
            binance_config.api_url = api_url;
        }
        if let Some(sapi_api_url) = config.sapi_api_url {
            binance_config.sapi_api_url = sapi_api_url;
        }
        if let Some(web_api_url) = config.web_api_url {
            binance_config.web_api_url = web_api_url;
        }
        if let Some(ws_stream_url) = config.ws_stream_url {
            binance_config.ws_stream_url = ws_stream_url;
        }
        if let Some(api_timeout_ms) = config.api_timeout_ms {
            binance_config.api_timeout_ms = api_timeout_ms;
        }
        if let Some(recv_window_ms) = config.recv_window_ms {
            binance_config.recv_window_ms = recv_window_ms;
        }
        if let Some(proxy_url) = config.proxy_url {
            binance_config.proxy_url = Some(proxy_url);
        }

        let credentials = BinanceCredentials::new(config.api_key, config.api_secret);
        let client = BinanceClient::with_config(Some(credentials), binance_config)
            .map_err(Error::from_binance)?;
        Ok(Self {
            account: BinanceAccount::new(client.clone()),
            market: BinanceMarket::new(client),
        })
    }

    pub(crate) async fn ticker(&self, instrument: &Instrument) -> Result<Ticker> {
        let exchange = ExchangeId::Binance;
        let symbol = instrument.symbol_for(exchange);
        let raw = self
            .market
            .get_ticker_24hr(Some(&symbol))
            .await
            .map_err(Error::from_binance)?;
        let object = raw.as_object().ok_or_else(|| Error::Adapter {
            exchange,
            message: "Binance ticker response is not an object".to_string(),
        })?;

        Ok(Ticker {
            exchange,
            instrument: instrument.clone(),
            exchange_symbol: symbol,
            last_price: string_field(object, "lastPrice").unwrap_or_default(),
            bid_price: string_field(object, "bidPrice"),
            ask_price: string_field(object, "askPrice"),
            volume_24h: string_field(object, "volume"),
            timestamp: u64_field(object, "closeTime"),
            raw,
        })
    }

    pub(crate) async fn balances(&self) -> Result<Vec<Balance>> {
        let balances = self
            .account
            .get_balance()
            .await
            .map_err(Error::from_binance)?;

        balances
            .into_iter()
            .map(|balance| {
                let raw = serde_json::to_value(&balance)?;
                Ok(Balance {
                    exchange: ExchangeId::Binance,
                    asset: balance.asset,
                    total: balance.balance,
                    available: balance.available_balance,
                    frozen: non_empty(balance.cross_un_pnl),
                    raw,
                })
            })
            .collect()
    }
}

fn string_field(object: &serde_json::Map<String, Value>, field: &str) -> Option<String> {
    object
        .get(field)
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn u64_field(object: &serde_json::Map<String, Value>, field: &str) -> Option<u64> {
    object.get(field).and_then(Value::as_u64)
}

fn non_empty(value: String) -> Option<String> {
    if value.is_empty() { None } else { Some(value) }
}