use std::convert::{TryFrom, TryInto}; use async_trait::async_trait; use bitfinex::api::Bitfinex; use bitfinex::orders::{OrderForm, OrderMeta}; use bitfinex::ticker::TradingPairTicker; use crate::currency::{Symbol, SymbolPair}; use crate::models::{Order, OrderKind, Position, PositionState}; use crate::BoxError; use std::sync::Arc; #[derive(Eq, PartialEq, Hash, Clone)] pub enum ExchangeKind { Bitfinex { api_key: String, api_secret: String, affiliate_code: Option, }, } /// You do **not** have to wrap the `Client` it in an [`Rc`] or [`Arc`] to **reuse** it, /// because it already uses an [`Arc`] internally. #[derive(Clone)] pub struct Client { inner: Arc>, } impl Client { pub fn new(exchange: ExchangeKind) -> Self { let inner = match exchange { ExchangeKind::Bitfinex { api_key, api_secret, affiliate_code, } => BitfinexConnector::new(&api_key, &api_secret).with_affiliate_code(affiliate_code), }; Client { inner: Arc::new(Box::new(inner)), } } } #[async_trait] pub trait Connector { async fn active_positions(&self, pair: &SymbolPair) -> Result, BoxError>; async fn current_prices(&self, pair: &SymbolPair) -> Result; async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError>; async fn submit_order( &self, pair: &SymbolPair, amount: f64, price: f64, kind: &OrderKind, ) -> Result<(), BoxError>; } pub struct BitfinexConnector { bfx: Bitfinex, affiliate_code: Option, // account_info: String, // ledger: String, } impl BitfinexConnector { pub fn new(api_key: &str, api_secret: &str) -> Self { BitfinexConnector { bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())), affiliate_code: None, } } pub fn with_affiliate_code(mut self, affiliate_code: Option) -> Self { self.affiliate_code = affiliate_code; self } } 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, } } } #[async_trait] impl Connector for BitfinexConnector { async fn active_positions(&self, pair: &SymbolPair) -> Result, BoxError> { let active_positions = self.bfx.positions.active_positions().await?; Ok(active_positions .into_iter() .filter_map(|x| x.try_into().ok()) .filter(|x: &Position| x.pair() == pair) .collect()) } async fn current_prices(&self, pair: &SymbolPair) -> Result { let ticker: TradingPairTicker = self.bfx.ticker.trading_pair(pair.clone()).await?; Ok(ticker) } async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { unimplemented!() } async fn submit_order( &self, pair: &SymbolPair, amount: f64, price: f64, kind: &OrderKind, ) -> Result<(), BoxError> { let order_form = match &self.affiliate_code { Some(affiliate_code) => OrderForm::new(pair.trading_repr(), price, amount, kind.into()) .with_meta(OrderMeta::new(affiliate_code.clone())), None => OrderForm::new(pair.trading_repr(), price, amount, kind.into()), }; Ok(self.bfx.orders.submit_order(&order_form).await?) } }