2021-01-23 13:44:08 +00:00
|
|
|
use std::convert::{TryFrom, TryInto};
|
|
|
|
use std::fmt::{Debug, Formatter};
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2021-01-06 21:17:01 +00:00
|
|
|
use async_trait::async_trait;
|
|
|
|
use bitfinex::api::Bitfinex;
|
2021-01-28 20:07:26 +00:00
|
|
|
use bitfinex::book::BookPrecision;
|
2021-01-27 20:18:06 +00:00
|
|
|
use bitfinex::orders::{CancelOrderForm, OrderMeta};
|
2021-01-28 20:06:11 +00:00
|
|
|
use bitfinex::responses::{OrderResponse, TradeResponse};
|
2021-01-06 21:17:01 +00:00
|
|
|
use bitfinex::ticker::TradingPairTicker;
|
2021-01-28 20:07:26 +00:00
|
|
|
use futures_retry::RetryPolicy;
|
2021-01-26 11:15:04 +00:00
|
|
|
use log::trace;
|
2021-01-26 17:13:14 +00:00
|
|
|
use tokio::macros::support::Future;
|
2021-01-26 11:15:04 +00:00
|
|
|
use tokio::time::Duration;
|
2021-01-06 21:17:01 +00:00
|
|
|
|
2021-02-20 21:01:32 +00:00
|
|
|
use crate::BoxError;
|
2021-01-27 20:18:06 +00:00
|
|
|
use crate::currency::{Symbol, SymbolPair};
|
2021-01-19 21:29:02 +00:00
|
|
|
use crate::models::{
|
2021-01-28 20:06:11 +00:00
|
|
|
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
|
2021-02-12 14:58:43 +00:00
|
|
|
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
|
2021-01-19 21:29:02 +00:00
|
|
|
};
|
2021-01-06 21:17:01 +00:00
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
|
|
|
pub enum Exchange {
|
|
|
|
Bitfinex,
|
|
|
|
}
|
|
|
|
|
2021-01-14 12:42:23 +00:00
|
|
|
#[derive(Eq, PartialEq, Hash, Clone, Debug)]
|
2021-01-23 16:13:37 +00:00
|
|
|
pub enum ExchangeDetails {
|
2021-01-26 16:19:10 +00:00
|
|
|
Bitfinex { api_key: String, api_secret: String },
|
2021-01-13 09:24:59 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 12:42:23 +00:00
|
|
|
/// You do **not** have to wrap the `Client` in an [`Rc`] or [`Arc`] to **reuse** it,
|
2021-01-13 09:24:59 +00:00
|
|
|
/// because it already uses an [`Arc`] internally.
|
2021-01-14 12:42:23 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2021-01-13 09:24:59 +00:00
|
|
|
pub struct Client {
|
2021-01-25 13:17:13 +00:00
|
|
|
exchange: Exchange,
|
2021-01-26 16:19:10 +00:00
|
|
|
inner: Arc<Box<dyn Connector>>,
|
2021-01-13 09:24:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
2021-01-23 16:13:37 +00:00
|
|
|
pub fn new(exchange: &ExchangeDetails) -> Self {
|
2021-01-25 13:17:13 +00:00
|
|
|
match exchange {
|
2021-01-23 16:13:37 +00:00
|
|
|
ExchangeDetails::Bitfinex {
|
2021-01-26 16:19:10 +00:00
|
|
|
api_key,
|
|
|
|
api_secret,
|
2021-01-25 13:17:13 +00:00
|
|
|
} => Self {
|
|
|
|
exchange: Exchange::Bitfinex,
|
2021-01-27 17:01:26 +00:00
|
|
|
inner: Arc::new(Box::new(BitfinexConnector::new(api_key, api_secret))),
|
2021-01-25 13:17:13 +00:00
|
|
|
},
|
2021-01-13 09:24:59 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-14 12:42:23 +00:00
|
|
|
|
2021-01-14 18:36:56 +00:00
|
|
|
pub async fn active_positions(
|
|
|
|
&self,
|
|
|
|
pair: &SymbolPair,
|
|
|
|
) -> Result<Option<Vec<Position>>, BoxError> {
|
2021-01-24 15:42:54 +00:00
|
|
|
// retrieving open positions and order book to calculate effective profit/loss
|
2021-02-13 14:58:15 +00:00
|
|
|
let (positions, order_book, fees) = tokio::join!(
|
2021-01-26 16:19:10 +00:00
|
|
|
self.inner.active_positions(pair),
|
2021-02-13 14:58:15 +00:00
|
|
|
self.inner.order_book(pair),
|
|
|
|
self.inner.trading_fees()
|
2021-01-24 15:42:54 +00:00
|
|
|
);
|
|
|
|
|
2021-02-13 14:58:15 +00:00
|
|
|
let (mut positions, order_book, fees) = (positions?, order_book?, fees?);
|
2021-01-24 15:42:54 +00:00
|
|
|
let (best_ask, best_bid) = (order_book.lowest_ask(), order_book.highest_bid());
|
|
|
|
|
2021-02-16 18:17:48 +00:00
|
|
|
if positions.is_none() {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2021-02-13 14:58:15 +00:00
|
|
|
let derivative_taker = fees
|
|
|
|
.iter()
|
|
|
|
.filter_map(|x| match x {
|
|
|
|
TradingFees::Taker {
|
|
|
|
platform,
|
|
|
|
percentage,
|
|
|
|
} if platform == &TradingPlatform::Derivative => Some(percentage),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.next()
|
|
|
|
.ok_or("Could not retrieve derivative taker fee!")?;
|
|
|
|
let margin_taker = fees
|
|
|
|
.iter()
|
|
|
|
.filter_map(|x| match x {
|
|
|
|
TradingFees::Taker {
|
|
|
|
platform,
|
|
|
|
percentage,
|
|
|
|
} if platform == &TradingPlatform::Margin => Some(percentage),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.next()
|
|
|
|
.ok_or("Could not retrieve margin taker fee!")?;
|
|
|
|
|
2021-01-24 15:42:54 +00:00
|
|
|
// updating positions with effective profit/loss
|
2021-01-24 20:53:33 +00:00
|
|
|
positions.iter_mut().flatten().for_each(|x| {
|
2021-02-13 14:58:15 +00:00
|
|
|
let fee = match x.platform() {
|
|
|
|
TradingPlatform::Funding | TradingPlatform::Exchange => {
|
|
|
|
unimplemented!()
|
|
|
|
}
|
|
|
|
TradingPlatform::Margin => margin_taker,
|
|
|
|
TradingPlatform::Derivative => derivative_taker,
|
|
|
|
};
|
|
|
|
|
2021-01-24 15:42:54 +00:00
|
|
|
if x.is_short() {
|
2021-02-13 14:58:15 +00:00
|
|
|
x.update_profit_loss(best_ask, *fee);
|
2021-01-24 15:42:54 +00:00
|
|
|
} else {
|
2021-02-13 14:58:15 +00:00
|
|
|
x.update_profit_loss(best_bid, *fee);
|
2021-01-24 15:42:54 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Ok(positions)
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
|
2021-01-26 16:19:10 +00:00
|
|
|
self.inner.current_prices(pair).await
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
2021-01-22 15:37:53 +00:00
|
|
|
pub async fn active_orders(&self, pair: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
|
2021-01-26 17:13:14 +00:00
|
|
|
Ok(self
|
|
|
|
.inner
|
|
|
|
.active_orders(pair)
|
|
|
|
.await?
|
|
|
|
.into_iter()
|
2021-02-17 16:23:34 +00:00
|
|
|
.filter(|x| &x.pair() == &pair)
|
2021-01-26 17:13:14 +00:00
|
|
|
.collect())
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
pub async fn submit_order(&self, order: &OrderForm) -> Result<ActiveOrder, BoxError> {
|
2021-01-26 16:19:10 +00:00
|
|
|
self.inner.submit_order(order).await
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
2021-01-19 21:29:02 +00:00
|
|
|
|
|
|
|
pub async fn order_book(&self, pair: &SymbolPair) -> Result<OrderBook, BoxError> {
|
2021-01-26 16:19:10 +00:00
|
|
|
self.inner.order_book(pair).await
|
2021-01-19 21:29:02 +00:00
|
|
|
}
|
2021-01-23 13:44:08 +00:00
|
|
|
|
|
|
|
pub async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
|
2021-01-26 16:19:10 +00:00
|
|
|
self.inner.cancel_order(order).await
|
2021-01-23 13:44:08 +00:00
|
|
|
}
|
2021-01-27 20:18:06 +00:00
|
|
|
|
|
|
|
pub async fn transfer_between_wallets(
|
|
|
|
&self,
|
|
|
|
from: &WalletKind,
|
|
|
|
to: &WalletKind,
|
|
|
|
symbol: Symbol,
|
|
|
|
amount: f64,
|
|
|
|
) -> Result<(), BoxError> {
|
|
|
|
self.inner
|
|
|
|
.transfer_between_wallets(from, to, symbol, amount)
|
|
|
|
.await
|
|
|
|
}
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
pub async fn trades_from_order(
|
|
|
|
&self,
|
|
|
|
order: &OrderDetails,
|
|
|
|
) -> Result<Option<Vec<Trade>>, BoxError> {
|
|
|
|
self.inner.trades_from_order(order).await
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn orders_history(
|
|
|
|
&self,
|
|
|
|
pair: &SymbolPair,
|
|
|
|
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
|
|
|
self.inner.orders_history(pair).await
|
|
|
|
}
|
2021-02-20 21:01:32 +00:00
|
|
|
|
|
|
|
pub async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { self.inner.trading_fees().await }
|
2021-01-11 17:16:44 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 21:17:01 +00:00
|
|
|
#[async_trait]
|
2021-01-14 12:42:23 +00:00
|
|
|
pub trait Connector: Send + Sync {
|
2021-01-15 10:40:36 +00:00
|
|
|
fn name(&self) -> String;
|
2021-01-14 18:36:56 +00:00
|
|
|
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError>;
|
2021-01-06 21:17:01 +00:00
|
|
|
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError>;
|
2021-01-19 21:29:02 +00:00
|
|
|
async fn order_book(&self, pair: &SymbolPair) -> Result<OrderBook, BoxError>;
|
2021-01-22 15:37:53 +00:00
|
|
|
async fn active_orders(&self, pair: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError>;
|
2021-01-23 16:13:37 +00:00
|
|
|
async fn submit_order(&self, order: &OrderForm) -> Result<ActiveOrder, BoxError>;
|
2021-01-23 13:44:08 +00:00
|
|
|
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError>;
|
2021-01-27 20:18:06 +00:00
|
|
|
async fn transfer_between_wallets(
|
|
|
|
&self,
|
|
|
|
from: &WalletKind,
|
|
|
|
to: &WalletKind,
|
|
|
|
symbol: Symbol,
|
|
|
|
amount: f64,
|
|
|
|
) -> Result<(), BoxError>;
|
2021-01-28 20:06:11 +00:00
|
|
|
async fn trades_from_order(&self, order: &OrderDetails)
|
2021-02-20 21:01:32 +00:00
|
|
|
-> Result<Option<Vec<Trade>>, BoxError>;
|
2021-01-28 20:06:11 +00:00
|
|
|
async fn orders_history(
|
|
|
|
&self,
|
|
|
|
pair: &SymbolPair,
|
|
|
|
) -> Result<Option<Vec<OrderDetails>>, BoxError>;
|
2021-02-12 14:58:43 +00:00
|
|
|
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>;
|
2021-01-06 21:17:01 +00:00
|
|
|
}
|
|
|
|
|
2021-01-14 12:42:23 +00:00
|
|
|
impl Debug for dyn Connector {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
2021-01-15 10:40:36 +00:00
|
|
|
write!(f, "{}", self.name())
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**************
|
|
|
|
* BITFINEX
|
|
|
|
**************/
|
|
|
|
|
2021-01-13 09:04:58 +00:00
|
|
|
pub struct BitfinexConnector {
|
2021-01-06 21:17:01 +00:00
|
|
|
bfx: Bitfinex,
|
|
|
|
}
|
|
|
|
|
2021-01-13 09:04:58 +00:00
|
|
|
impl BitfinexConnector {
|
2021-01-15 10:40:36 +00:00
|
|
|
const AFFILIATE_CODE: &'static str = "XPebOgHxA";
|
|
|
|
|
2021-01-26 11:15:04 +00:00
|
|
|
fn handle_small_nonce_error(e: BoxError) -> RetryPolicy<BoxError> {
|
|
|
|
if e.to_string().contains("nonce: small") {
|
|
|
|
return RetryPolicy::WaitRetry(Duration::from_millis(1));
|
|
|
|
}
|
2021-02-13 15:52:50 +00:00
|
|
|
RetryPolicy::ForwardError(e)
|
2021-01-26 11:15:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-06 21:17:01 +00:00
|
|
|
pub fn new(api_key: &str, api_secret: &str) -> Self {
|
2021-01-13 09:04:58 +00:00
|
|
|
BitfinexConnector {
|
2021-01-06 21:17:01 +00:00
|
|
|
bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
fn format_trading_pair(pair: &SymbolPair) -> String {
|
2021-02-13 15:52:50 +00:00
|
|
|
if pair.to_string().to_lowercase().contains("test")
|
|
|
|
|| pair.to_string().to_lowercase().contains("f0")
|
|
|
|
{
|
2021-01-14 12:42:23 +00:00
|
|
|
format!("{}:{}", pair.base(), pair.quote())
|
|
|
|
} else {
|
2021-02-13 15:52:50 +00:00
|
|
|
format!("{}{}", pair.base(), pair.quote())
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
// retry to submit the request until it succeeds.
|
|
|
|
// the function may fail due to concurrent signed requests
|
|
|
|
// parsed in different times by the server
|
|
|
|
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
|
2021-02-20 21:01:32 +00:00
|
|
|
where
|
|
|
|
F: FnMut() -> Fut,
|
|
|
|
Fut: Future<Output=Result<O, BoxError>>,
|
2021-01-28 20:06:11 +00:00
|
|
|
{
|
|
|
|
let response = {
|
|
|
|
loop {
|
|
|
|
match func().await {
|
|
|
|
Ok(response) => break response,
|
|
|
|
Err(e) => {
|
|
|
|
if !e.to_string().contains("nonce: small") {
|
|
|
|
return Err(e);
|
|
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_nanos(1)).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(response)
|
|
|
|
}
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl Connector for BitfinexConnector {
|
2021-01-15 10:40:36 +00:00
|
|
|
fn name(&self) -> String {
|
|
|
|
"Bitfinex".into()
|
|
|
|
}
|
|
|
|
|
2021-01-14 18:36:56 +00:00
|
|
|
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
|
2021-01-28 20:06:11 +00:00
|
|
|
let active_positions =
|
|
|
|
BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?;
|
2021-01-14 12:42:23 +00:00
|
|
|
|
2021-01-14 18:36:56 +00:00
|
|
|
let positions: Vec<_> = active_positions
|
2021-01-14 12:42:23 +00:00
|
|
|
.into_iter()
|
|
|
|
.filter_map(|x| x.try_into().ok())
|
|
|
|
.filter(|x: &Position| x.pair() == pair)
|
2021-01-14 18:36:56 +00:00
|
|
|
.collect();
|
|
|
|
|
2021-02-21 18:38:20 +00:00
|
|
|
trace!("\tRetrieved positions for {}", pair);
|
2021-01-26 11:15:04 +00:00
|
|
|
Ok((!positions.is_empty()).then_some(positions))
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
|
2021-01-23 16:13:37 +00:00
|
|
|
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
|
|
|
|
|
|
|
let ticker: TradingPairTicker = self.bfx.ticker.trading_pair(symbol_name).await?;
|
2021-01-14 12:42:23 +00:00
|
|
|
|
|
|
|
Ok(ticker)
|
|
|
|
}
|
|
|
|
|
2021-01-28 20:06:11 +00:00
|
|
|
async fn order_book(&self, pair: &SymbolPair) -> Result<OrderBook, BoxError> {
|
|
|
|
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
|
|
|
|
|
|
|
let response = BitfinexConnector::retry_nonce(|| {
|
|
|
|
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0)
|
|
|
|
})
|
2021-02-20 21:01:32 +00:00
|
|
|
.await?;
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
let entries = response
|
|
|
|
.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))
|
|
|
|
}
|
|
|
|
|
2021-01-24 13:41:18 +00:00
|
|
|
async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
|
2021-01-28 20:06:11 +00:00
|
|
|
let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?;
|
2021-01-24 13:41:18 +00:00
|
|
|
|
|
|
|
Ok(response.iter().map(Into::into).collect())
|
2021-01-14 12:42:23 +00:00
|
|
|
}
|
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
async fn submit_order(&self, order: &OrderForm) -> Result<ActiveOrder, BoxError> {
|
|
|
|
let symbol_name = format!("t{}", BitfinexConnector::format_trading_pair(order.pair()));
|
2021-02-13 15:29:00 +00:00
|
|
|
let amount = order.amount();
|
2021-01-23 16:13:37 +00:00
|
|
|
|
2021-02-13 15:29:00 +00:00
|
|
|
let order_form = {
|
2021-02-24 11:09:56 +00:00
|
|
|
match order.kind() {
|
|
|
|
OrderKind::Limit { price } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
|
|
|
}
|
|
|
|
OrderKind::Market => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
|
|
|
}
|
|
|
|
OrderKind::Stop { price } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
|
|
|
}
|
|
|
|
OrderKind::StopLimit { stop_price: price, limit_price } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
|
|
|
.with_price_aux_limit(Some(limit_price))?
|
|
|
|
}
|
|
|
|
OrderKind::TrailingStop { distance } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
|
|
|
.with_price_trailing(Some(distance))?
|
|
|
|
}
|
|
|
|
OrderKind::FillOrKill { price } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
|
|
|
}
|
|
|
|
OrderKind::ImmediateOrCancel { price } => {
|
|
|
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
2021-02-13 15:29:00 +00:00
|
|
|
}
|
2021-01-23 16:13:37 +00:00
|
|
|
}
|
2021-02-24 11:09:56 +00:00
|
|
|
.with_meta(Some(OrderMeta::new(
|
|
|
|
BitfinexConnector::AFFILIATE_CODE.to_string(),
|
|
|
|
)))
|
|
|
|
// TODO: CHANGEME!
|
|
|
|
.with_leverage(Some(15))
|
2021-02-13 15:29:00 +00:00
|
|
|
};
|
2021-01-14 12:42:23 +00:00
|
|
|
|
2021-01-28 20:06:11 +00:00
|
|
|
let response =
|
|
|
|
BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?;
|
2021-01-19 21:29:02 +00:00
|
|
|
|
2021-02-22 00:23:40 +00:00
|
|
|
// parsing response into ActiveOrder and adding leverage from order form
|
|
|
|
let order_response: ActiveOrder = (&response).try_into()?;
|
|
|
|
|
|
|
|
// TODO: CHANGEME!!!!
|
|
|
|
Ok(order_response.with_leverage(Some(15.0)))
|
2021-01-19 21:29:02 +00:00
|
|
|
}
|
|
|
|
|
2021-01-23 13:44:08 +00:00
|
|
|
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
|
2021-01-23 16:13:37 +00:00
|
|
|
let cancel_form = order.into();
|
2021-01-23 13:44:08 +00:00
|
|
|
|
2021-01-28 20:06:11 +00:00
|
|
|
let response =
|
|
|
|
BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?;
|
2021-01-26 11:15:04 +00:00
|
|
|
|
|
|
|
Ok((&response).try_into()?)
|
2021-01-23 16:13:37 +00:00
|
|
|
}
|
2021-01-27 20:18:06 +00:00
|
|
|
|
|
|
|
async fn transfer_between_wallets(
|
|
|
|
&self,
|
|
|
|
from: &WalletKind,
|
|
|
|
to: &WalletKind,
|
|
|
|
symbol: Symbol,
|
|
|
|
amount: f64,
|
|
|
|
) -> Result<(), BoxError> {
|
2021-01-28 20:06:11 +00:00
|
|
|
BitfinexConnector::retry_nonce(|| {
|
|
|
|
self.bfx.account.transfer_between_wallets(
|
|
|
|
from.into(),
|
|
|
|
to.into(),
|
|
|
|
symbol.to_string(),
|
|
|
|
amount,
|
|
|
|
)
|
|
|
|
})
|
2021-02-20 21:01:32 +00:00
|
|
|
.await?;
|
2021-01-27 20:18:06 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
async fn trades_from_order(
|
|
|
|
&self,
|
|
|
|
order: &OrderDetails,
|
|
|
|
) -> Result<Option<Vec<Trade>>, BoxError> {
|
|
|
|
let response = BitfinexConnector::retry_nonce(|| {
|
|
|
|
self.bfx
|
|
|
|
.trades
|
|
|
|
.generated_by_order(order.pair().trading_repr(), order.id())
|
|
|
|
})
|
2021-02-20 21:01:32 +00:00
|
|
|
.await?;
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
if response.is_empty() {
|
|
|
|
Ok(None)
|
|
|
|
} else {
|
|
|
|
Ok(Some(response.iter().map(Into::into).collect()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn orders_history(
|
|
|
|
&self,
|
|
|
|
pair: &SymbolPair,
|
|
|
|
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
|
|
|
let response =
|
|
|
|
BitfinexConnector::retry_nonce(|| self.bfx.orders.history(Some(pair.trading_repr())))
|
|
|
|
.await?;
|
|
|
|
let mapped_vec: Vec<_> = response.iter().map(Into::into).collect();
|
|
|
|
|
|
|
|
Ok((!mapped_vec.is_empty()).then_some(mapped_vec))
|
|
|
|
}
|
2021-02-12 14:58:43 +00:00
|
|
|
|
|
|
|
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> {
|
2021-02-13 14:58:15 +00:00
|
|
|
let mut fees = vec![];
|
2021-02-16 18:17:48 +00:00
|
|
|
let accountfees =
|
|
|
|
BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?;
|
2021-02-13 14:58:15 +00:00
|
|
|
|
|
|
|
// Derivatives
|
|
|
|
let derivative_taker = TradingFees::Taker {
|
|
|
|
platform: TradingPlatform::Derivative,
|
|
|
|
percentage: accountfees.derivative_taker() * 100.0,
|
|
|
|
};
|
|
|
|
let derivative_maker = TradingFees::Maker {
|
|
|
|
platform: TradingPlatform::Derivative,
|
|
|
|
percentage: accountfees.derivative_rebate() * 100.0,
|
|
|
|
};
|
|
|
|
fees.push(derivative_taker);
|
|
|
|
fees.push(derivative_maker);
|
|
|
|
|
|
|
|
// Exchange
|
|
|
|
let exchange_taker = TradingFees::Taker {
|
|
|
|
platform: TradingPlatform::Exchange,
|
|
|
|
percentage: accountfees.taker_to_fiat() * 100.0,
|
|
|
|
};
|
|
|
|
let exchange_maker = TradingFees::Maker {
|
|
|
|
platform: TradingPlatform::Exchange,
|
|
|
|
percentage: accountfees.maker_fee() * 100.0,
|
|
|
|
};
|
|
|
|
fees.push(exchange_taker);
|
|
|
|
fees.push(exchange_maker);
|
|
|
|
|
|
|
|
// Margin
|
|
|
|
let margin_taker = TradingFees::Taker {
|
|
|
|
platform: TradingPlatform::Margin,
|
|
|
|
percentage: accountfees.taker_to_fiat() * 100.0,
|
|
|
|
};
|
|
|
|
let margin_maker = TradingFees::Maker {
|
|
|
|
platform: TradingPlatform::Margin,
|
|
|
|
percentage: accountfees.maker_fee() * 100.0,
|
|
|
|
};
|
|
|
|
fees.push(margin_taker);
|
|
|
|
fees.push(margin_maker);
|
|
|
|
|
|
|
|
Ok(fees)
|
2021-02-12 14:58:43 +00:00
|
|
|
}
|
2021-01-23 16:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&ActiveOrder> for CancelOrderForm {
|
|
|
|
fn from(o: &ActiveOrder) -> Self {
|
2021-02-17 16:23:34 +00:00
|
|
|
Self::from_id(o.id())
|
2021-01-23 13:44:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-27 20:18:06 +00:00
|
|
|
impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder {
|
2021-01-23 13:44:08 +00:00
|
|
|
type Error = BoxError;
|
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
fn try_from(response: &OrderResponse) -> Result<Self, Self::Error> {
|
2021-02-17 16:23:34 +00:00
|
|
|
let pair = SymbolPair::from_str(response.symbol())?;
|
|
|
|
|
|
|
|
Ok(ActiveOrder::new(
|
|
|
|
Exchange::Bitfinex,
|
|
|
|
response.id(),
|
|
|
|
pair.clone(),
|
|
|
|
OrderForm::new(pair, response.into(), response.into(), response.amount()),
|
|
|
|
response.mts_create(),
|
|
|
|
response.mts_update(),
|
|
|
|
)
|
2021-02-20 21:01:32 +00:00
|
|
|
.with_group_id(response.gid())
|
|
|
|
.with_client_id(Some(response.cid())))
|
2021-01-23 13:44:08 +00:00
|
|
|
}
|
2021-01-06 21:17:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl TryInto<Position> for bitfinex::positions::Position {
|
|
|
|
type Error = BoxError;
|
|
|
|
|
|
|
|
fn try_into(self) -> Result<Position, Self::Error> {
|
|
|
|
let state = {
|
|
|
|
if self.status().to_lowercase().contains("active") {
|
|
|
|
PositionState::Open
|
|
|
|
} else {
|
|
|
|
PositionState::Closed
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-02-13 14:58:15 +00:00
|
|
|
let platform = {
|
|
|
|
if self.symbol().to_ascii_lowercase().contains("f0") {
|
|
|
|
TradingPlatform::Derivative
|
|
|
|
} else {
|
|
|
|
TradingPlatform::Margin
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-01-06 21:17:01 +00:00
|
|
|
Ok(Position::new(
|
2021-01-22 16:09:17 +00:00
|
|
|
SymbolPair::from_str(self.symbol())?,
|
2021-01-06 21:17:01 +00:00
|
|
|
state,
|
|
|
|
self.amount(),
|
|
|
|
self.base_price(),
|
|
|
|
self.pl(),
|
|
|
|
self.pl_perc(),
|
|
|
|
self.price_liq(),
|
|
|
|
self.position_id(),
|
2021-02-13 14:58:15 +00:00
|
|
|
platform,
|
2021-02-13 15:29:00 +00:00
|
|
|
self.leverage(),
|
2021-01-06 21:17:01 +00:00
|
|
|
)
|
2021-02-20 21:01:32 +00:00
|
|
|
.with_creation_date(self.mts_create())
|
|
|
|
.with_creation_update(self.mts_update()))
|
2021-01-06 21:17:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-23 16:13:37 +00:00
|
|
|
impl From<&OrderForm> for bitfinex::orders::OrderKind {
|
|
|
|
fn from(o: &OrderForm) -> Self {
|
|
|
|
match o.platform() {
|
|
|
|
TradingPlatform::Exchange => match o.kind() {
|
|
|
|
OrderKind::Limit { .. } => bitfinex::orders::OrderKind::ExchangeLimit,
|
|
|
|
OrderKind::Market { .. } => bitfinex::orders::OrderKind::ExchangeMarket,
|
|
|
|
OrderKind::Stop { .. } => bitfinex::orders::OrderKind::ExchangeStop,
|
|
|
|
OrderKind::StopLimit { .. } => bitfinex::orders::OrderKind::ExchangeStopLimit,
|
|
|
|
OrderKind::TrailingStop { .. } => bitfinex::orders::OrderKind::ExchangeTrailingStop,
|
|
|
|
OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::ExchangeFok,
|
|
|
|
OrderKind::ImmediateOrCancel { .. } => bitfinex::orders::OrderKind::ExchangeIoc,
|
|
|
|
},
|
2021-02-12 14:58:43 +00:00
|
|
|
TradingPlatform::Margin | TradingPlatform::Derivative => match o.kind() {
|
2021-01-23 16:13:37 +00:00
|
|
|
OrderKind::Limit { .. } => bitfinex::orders::OrderKind::Limit,
|
|
|
|
OrderKind::Market { .. } => bitfinex::orders::OrderKind::Market,
|
|
|
|
OrderKind::Stop { .. } => bitfinex::orders::OrderKind::Stop,
|
|
|
|
OrderKind::StopLimit { .. } => bitfinex::orders::OrderKind::StopLimit,
|
|
|
|
OrderKind::TrailingStop { .. } => bitfinex::orders::OrderKind::TrailingStop,
|
|
|
|
OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::Fok,
|
|
|
|
OrderKind::ImmediateOrCancel { .. } => bitfinex::orders::OrderKind::Ioc,
|
|
|
|
},
|
|
|
|
_ => unimplemented!(),
|
2021-01-11 17:16:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-27 20:18:06 +00:00
|
|
|
impl From<&bitfinex::responses::OrderResponse> for TradingPlatform {
|
2021-01-23 16:13:37 +00:00
|
|
|
fn from(response: &OrderResponse) -> Self {
|
|
|
|
match response.order_type() {
|
|
|
|
bitfinex::orders::OrderKind::Limit
|
|
|
|
| bitfinex::orders::OrderKind::Market
|
|
|
|
| bitfinex::orders::OrderKind::StopLimit
|
|
|
|
| bitfinex::orders::OrderKind::Stop
|
|
|
|
| bitfinex::orders::OrderKind::TrailingStop
|
|
|
|
| bitfinex::orders::OrderKind::Fok
|
|
|
|
| bitfinex::orders::OrderKind::Ioc => Self::Margin,
|
|
|
|
_ => Self::Exchange,
|
2021-01-22 16:09:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-24 13:41:18 +00:00
|
|
|
impl From<&bitfinex::orders::ActiveOrder> for TradingPlatform {
|
|
|
|
fn from(response: &bitfinex::orders::ActiveOrder) -> Self {
|
|
|
|
match response.order_type() {
|
|
|
|
bitfinex::orders::OrderKind::Limit
|
|
|
|
| bitfinex::orders::OrderKind::Market
|
|
|
|
| bitfinex::orders::OrderKind::StopLimit
|
|
|
|
| bitfinex::orders::OrderKind::Stop
|
|
|
|
| bitfinex::orders::OrderKind::TrailingStop
|
|
|
|
| bitfinex::orders::OrderKind::Fok
|
|
|
|
| bitfinex::orders::OrderKind::Ioc => Self::Margin,
|
|
|
|
_ => Self::Exchange,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-27 20:18:06 +00:00
|
|
|
impl From<&bitfinex::responses::OrderResponse> for OrderKind {
|
2021-01-23 16:13:37 +00:00
|
|
|
fn from(response: &OrderResponse) -> Self {
|
|
|
|
match response.order_type() {
|
|
|
|
bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => {
|
|
|
|
Self::Limit {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => {
|
2021-02-13 15:29:00 +00:00
|
|
|
Self::Market
|
2021-01-23 16:13:37 +00:00
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => {
|
|
|
|
Self::Stop {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::StopLimit
|
|
|
|
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
2021-02-20 21:01:32 +00:00
|
|
|
stop_price: response.price(),
|
2021-01-24 13:41:18 +00:00
|
|
|
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
|
|
|
},
|
|
|
|
bitfinex::orders::OrderKind::TrailingStop
|
|
|
|
| bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop {
|
|
|
|
distance: response.price_trailing().expect("Distance not found!"),
|
|
|
|
},
|
|
|
|
bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => {
|
|
|
|
Self::FillOrKill {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => {
|
|
|
|
Self::ImmediateOrCancel {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
|
|
|
|
fn from(response: &bitfinex::orders::ActiveOrder) -> Self {
|
|
|
|
match response.order_type() {
|
|
|
|
bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => {
|
|
|
|
Self::Limit {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => {
|
2021-02-13 15:29:00 +00:00
|
|
|
Self::Market {}
|
2021-01-24 13:41:18 +00:00
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => {
|
|
|
|
Self::Stop {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::StopLimit
|
|
|
|
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
2021-02-20 21:01:32 +00:00
|
|
|
stop_price: response.price(),
|
2021-01-24 13:41:18 +00:00
|
|
|
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
2021-01-23 16:13:37 +00:00
|
|
|
},
|
|
|
|
bitfinex::orders::OrderKind::TrailingStop
|
|
|
|
| bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop {
|
2021-01-24 13:41:18 +00:00
|
|
|
distance: response.price_trailing().expect("Distance not found!"),
|
2021-01-23 16:13:37 +00:00
|
|
|
},
|
|
|
|
bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => {
|
|
|
|
Self::FillOrKill {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => {
|
|
|
|
Self::ImmediateOrCancel {
|
|
|
|
price: response.price(),
|
|
|
|
}
|
|
|
|
}
|
2021-01-16 11:43:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-24 13:41:18 +00:00
|
|
|
impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder {
|
|
|
|
fn from(order: &bitfinex::orders::ActiveOrder) -> Self {
|
|
|
|
let pair = SymbolPair::from_str(&order.symbol()).expect("Invalid symbol!");
|
|
|
|
|
2021-02-17 16:23:34 +00:00
|
|
|
ActiveOrder::new(
|
|
|
|
Exchange::Bitfinex,
|
|
|
|
order.id(),
|
|
|
|
pair.clone(),
|
|
|
|
OrderForm::new(pair, order.into(), order.into(), order.amount()),
|
|
|
|
order.creation_timestamp(),
|
|
|
|
order.update_timestamp(),
|
|
|
|
)
|
2021-02-20 21:01:32 +00:00
|
|
|
.with_client_id(Some(order.client_id()))
|
|
|
|
.with_group_id(order.group_id())
|
2021-01-24 13:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-14 12:42:23 +00:00
|
|
|
impl From<TradingPairTicker> 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,
|
|
|
|
}
|
2021-01-06 21:17:01 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-27 20:18:06 +00:00
|
|
|
|
|
|
|
impl From<&WalletKind> for &bitfinex::account::WalletKind {
|
|
|
|
fn from(k: &WalletKind) -> Self {
|
|
|
|
match k {
|
|
|
|
WalletKind::Exchange => &bitfinex::account::WalletKind::Exchange,
|
|
|
|
WalletKind::Margin => &bitfinex::account::WalletKind::Margin,
|
|
|
|
WalletKind::Funding => &bitfinex::account::WalletKind::Funding,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-28 20:06:11 +00:00
|
|
|
|
|
|
|
impl From<&bitfinex::orders::ActiveOrder> for OrderDetails {
|
|
|
|
fn from(order: &bitfinex::orders::ActiveOrder) -> Self {
|
|
|
|
Self::new(
|
|
|
|
Exchange::Bitfinex,
|
|
|
|
order.id(),
|
|
|
|
SymbolPair::from_str(order.symbol()).unwrap(),
|
|
|
|
order.into(),
|
|
|
|
order.into(),
|
|
|
|
order.update_timestamp(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: fields are hardcoded, to fix
|
|
|
|
impl From<&bitfinex::responses::TradeResponse> for Trade {
|
|
|
|
fn from(response: &TradeResponse) -> Self {
|
2021-02-13 14:58:15 +00:00
|
|
|
let pair = SymbolPair::from_str(&response.symbol()).unwrap();
|
2021-01-28 20:06:11 +00:00
|
|
|
let fee = {
|
2021-02-13 14:58:15 +00:00
|
|
|
if response.is_maker() {
|
|
|
|
OrderFee::Maker(response.fee())
|
2021-01-28 20:06:11 +00:00
|
|
|
} else {
|
2021-02-13 14:58:15 +00:00
|
|
|
OrderFee::Taker(response.fee())
|
2021-01-28 20:06:11 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Self {
|
2021-02-13 14:58:15 +00:00
|
|
|
trade_id: response.trade_id(),
|
2021-01-28 20:06:11 +00:00
|
|
|
pair,
|
2021-02-13 14:58:15 +00:00
|
|
|
execution_timestamp: response.execution_timestamp(),
|
|
|
|
price: response.execution_price(),
|
|
|
|
amount: response.execution_amount(),
|
2021-01-28 20:06:11 +00:00
|
|
|
fee,
|
2021-02-13 15:52:50 +00:00
|
|
|
fee_currency: Symbol::new(response.symbol().to_owned()),
|
2021-01-28 20:06:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|