use async_trait::async_trait; use bitfinex::api::Bitfinex; use bitfinex::orders::{ActiveOrder, OrderMeta}; use bitfinex::ticker::TradingPairTicker; use log::debug; use std::convert::TryInto; use std::fmt::{Debug, Formatter}; use std::sync::Arc; use crate::currency::SymbolPair; use crate::models::{ ExecutedOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionState, PriceTicker, }; use crate::BoxError; #[derive(Eq, PartialEq, Hash, Clone, Debug)] pub enum ExchangeKind { Bitfinex { api_key: String, api_secret: String }, } /// You do **not** have to wrap the `Client` in an [`Rc`] or [`Arc`] to **reuse** it, /// because it already uses an [`Arc`] internally. #[derive(Clone, Debug)] pub struct Client { exchange: ExchangeKind, inner: Arc>, } impl Client { pub fn new(exchange: &ExchangeKind) -> Self { let inner = match &exchange { ExchangeKind::Bitfinex { api_key, api_secret, } => BitfinexConnector::new(&api_key, &api_secret), }; Client { exchange: exchange.clone(), inner: Arc::new(Box::new(inner)), } } pub async fn active_positions( &self, pair: &SymbolPair, ) -> Result>, BoxError> { self.inner.active_positions(pair).await } pub async fn current_prices(&self, pair: &SymbolPair) -> Result { self.inner.current_prices(pair).await } pub async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { self.inner.active_orders(pair).await } pub async fn submit_order(&self, order: OrderForm) -> Result { self.inner.submit_order(order).await } pub async fn order_book(&self, pair: &SymbolPair) -> Result { self.inner.order_book(pair).await } } #[async_trait] pub trait Connector: Send + Sync { fn name(&self) -> String; async fn active_positions(&self, pair: &SymbolPair) -> Result>, BoxError>; async fn current_prices(&self, pair: &SymbolPair) -> Result; async fn order_book(&self, pair: &SymbolPair) -> Result; async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError>; async fn submit_order(&self, order: OrderForm) -> Result; } impl Debug for dyn Connector { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.name()) } } /************** * BITFINEX **************/ pub struct BitfinexConnector { bfx: Bitfinex, affiliate_code: Option, // account_info: String, // ledger: String, } impl BitfinexConnector { const AFFILIATE_CODE: &'static str = "XPebOgHxA"; pub fn new(api_key: &str, api_secret: &str) -> Self { BitfinexConnector { bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())), affiliate_code: Some(BitfinexConnector::AFFILIATE_CODE.into()), } } fn format_trading_pair(&self, pair: &SymbolPair) -> String { if pair.to_string().to_lowercase().contains("test") { format!("{}:{}", pair.base(), pair.quote()) } else { format!("{}{}", pair.base(), pair.quote()) } } } #[async_trait] impl Connector for BitfinexConnector { fn name(&self) -> String { "Bitfinex".into() } async fn active_positions(&self, pair: &SymbolPair) -> Result>, BoxError> { let active_positions = self.bfx.positions.active_positions().await?; let positions: Vec<_> = active_positions .into_iter() .filter_map(|x| x.try_into().ok()) .filter(|x: &Position| x.pair() == pair) .collect(); if positions.is_empty() { Ok(None) } else { Ok(Some(positions)) } } async fn current_prices(&self, pair: &SymbolPair) -> Result { let ticker: TradingPairTicker = self .bfx .ticker .trading_pair(self.format_trading_pair(pair)) .await?; Ok(ticker) } async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { unimplemented!() } async fn submit_order(&self, order: OrderForm) -> Result { // TODO: change trading pair formatting. awful. let order_form = match &self.affiliate_code { Some(affiliate_code) => bitfinex::orders::OrderForm::new( format!("t{}", self.format_trading_pair(order.pair())), *order.price(), *order.amount(), order.kind().into(), ) .with_meta(OrderMeta::new(affiliate_code.clone())), None => bitfinex::orders::OrderForm::new( format!("t{}", self.format_trading_pair(order.pair())), *order.price(), *order.amount(), order.kind().into(), ), }; let response = self.bfx.orders.submit_order(&order_form).await?; Ok(ExecutedOrder { id: 1, group_id: None, client_id: 0, symbol: "".to_string(), creation_timestamp: 0, update_timestamp: 0, amount: 0.0, amount_original: 0.0, order_type: "".to_string(), previous_order_type: None, flags: None, order_status: None, price: 0.0, price_avg: 0.0, price_trailing: None, price_aux_limit: None, notify: 0, hidden: 0, placed_id: None, }) } async fn order_book(&self, pair: &SymbolPair) -> Result { let x = self .bfx .book .trading_pair(self.format_trading_pair(&pair), "P0".into()) .await?; let entries = x .into_iter() .map(|x| OrderBookEntry::Trading { price: x.price, count: x.count as u64, amount: x.amount, }) .collect(); Ok(OrderBook::new(pair.clone()).with_entries(entries)) } } impl TryInto for bitfinex::positions::Position { type Error = BoxError; fn try_into(self) -> Result { let state = { if self.status().to_lowercase().contains("active") { PositionState::Open } else { PositionState::Closed } }; Ok(Position::new( self.symbol().try_into()?, state, self.amount(), self.base_price(), self.pl(), self.pl_perc(), self.price_liq(), self.position_id(), ) .with_creation_date(self.mts_create()) .with_creation_update(self.mts_update())) } } impl From<&OrderKind> for bitfinex::orders::OrderKind { fn from(o: &OrderKind) -> Self { match o { OrderKind::Limit => Self::Limit, OrderKind::ExchangeLimit => Self::ExchangeLimit, OrderKind::Market => Self::Market, OrderKind::ExchangeMarket => Self::ExchangeMarket, OrderKind::Stop => Self::Stop, OrderKind::ExchangeStop => Self::ExchangeStop, OrderKind::StopLimit => Self::StopLimit, OrderKind::ExchangeStopLimit => Self::ExchangeStopLimit, OrderKind::TrailingStop => Self::TrailingStop, OrderKind::Fok => Self::Fok, OrderKind::ExchangeFok => Self::ExchangeFok, OrderKind::Ioc => Self::Ioc, OrderKind::ExchangeIoc => Self::ExchangeIoc, } } } impl From for bitfinex::orders::OrderKind { fn from(o: OrderKind) -> Self { match o { OrderKind::Limit => Self::Limit, OrderKind::ExchangeLimit => Self::ExchangeLimit, OrderKind::Market => Self::Market, OrderKind::ExchangeMarket => Self::ExchangeMarket, OrderKind::Stop => Self::Stop, OrderKind::ExchangeStop => Self::ExchangeStop, OrderKind::StopLimit => Self::StopLimit, OrderKind::ExchangeStopLimit => Self::ExchangeStopLimit, OrderKind::TrailingStop => Self::TrailingStop, OrderKind::Fok => Self::Fok, OrderKind::ExchangeFok => Self::ExchangeFok, OrderKind::Ioc => Self::Ioc, OrderKind::ExchangeIoc => Self::ExchangeIoc, } } } impl From for PriceTicker { fn from(t: TradingPairTicker) -> Self { Self { bid: t.bid, bid_size: t.bid_size, ask: t.ask, ask_size: t.ask_size, daily_change: t.daily_change, daily_change_perc: t.daily_change_perc, last_price: t.last_price, volume: t.volume, high: t.high, low: t.low, } } } impl From for ExecutedOrder { fn from(o: ActiveOrder) -> Self { Self { id: o.id, group_id: o.group_id, client_id: o.client_id, symbol: o.symbol, creation_timestamp: o.creation_timestamp, update_timestamp: o.update_timestamp, amount: o.amount, amount_original: o.amount_original, order_type: o.order_type, previous_order_type: o.previous_order_type, flags: o.flags, order_status: o.order_status, price: o.price, price_avg: o.price_avg, price_trailing: o.price_trailing, price_aux_limit: o.price_aux_limit, notify: o.notify, hidden: o.hidden, placed_id: o.placed_id, } } }