From d383328ebb9dea6ffb16dfcaa26d56e27e70cebc Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Thu, 28 Jan 2021 20:06:11 +0000 Subject: [PATCH] implemented trades from orders and orders history --- rustybot/src/connectors.rs | 243 ++++++++++++++++++++++++------------- rustybot/src/currency.rs | 4 +- rustybot/src/managers.rs | 2 +- rustybot/src/models.rs | 71 ++++++++++- rustybot/src/strategy.rs | 12 +- 5 files changed, 234 insertions(+), 98 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index cc60ea7..0bf4c7c 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -5,20 +5,23 @@ use std::sync::Arc; use async_trait::async_trait; use bitfinex::api::Bitfinex; +use bitfinex::book::{Book, BookPrecision}; use bitfinex::orders::{CancelOrderForm, OrderMeta}; +use bitfinex::responses::{OrderResponse, TradeResponse}; use bitfinex::ticker::TradingPairTicker; use futures_retry::{FutureRetry, RetryPolicy}; use log::trace; use tokio::macros::support::Future; use tokio::time::Duration; +use tokio_tungstenite::stream::Stream::Plain; use crate::currency::{Symbol, SymbolPair}; +use crate::models::TradingPlatform::Margin; use crate::models::{ - ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionState, - PriceTicker, TradingPlatform, WalletKind, + ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, + PositionState, PriceTicker, Trade, TradingPlatform, WalletKind, }; use crate::BoxError; -use bitfinex::responses::OrderResponse; #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum Exchange { @@ -114,6 +117,20 @@ impl Client { .transfer_between_wallets(from, to, symbol, amount) .await } + + pub async fn trades_from_order( + &self, + order: &OrderDetails, + ) -> Result>, BoxError> { + self.inner.trades_from_order(order).await + } + + pub async fn orders_history( + &self, + pair: &SymbolPair, + ) -> Result>, BoxError> { + self.inner.orders_history(pair).await + } } #[async_trait] @@ -132,6 +149,12 @@ pub trait Connector: Send + Sync { symbol: Symbol, amount: f64, ) -> Result<(), BoxError>; + async fn trades_from_order(&self, order: &OrderDetails) + -> Result>, BoxError>; + async fn orders_history( + &self, + pair: &SymbolPair, + ) -> Result>, BoxError>; } impl Debug for dyn Connector { @@ -171,6 +194,31 @@ impl BitfinexConnector { format!("{}{}", pair.base(), pair.quote()) } } + + // 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(mut func: F) -> Result + where + F: FnMut() -> Fut, + Fut: Future>, + { + 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) + } } #[async_trait] @@ -180,12 +228,8 @@ impl Connector for BitfinexConnector { } async fn active_positions(&self, pair: &SymbolPair) -> Result>, BoxError> { - let (active_positions, _) = FutureRetry::new( - move || self.bfx.positions.active_positions(), - BitfinexConnector::handle_small_nonce_error, - ) - .await - .map_err(|(e, _attempts)| e)?; + let active_positions = + BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?; let positions: Vec<_> = active_positions .into_iter() @@ -205,13 +249,28 @@ impl Connector for BitfinexConnector { Ok(ticker) } + async fn order_book(&self, pair: &SymbolPair) -> Result { + let symbol_name = BitfinexConnector::format_trading_pair(pair); + + let response = BitfinexConnector::retry_nonce(|| { + self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0) + }) + .await?; + + 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)) + } + async fn active_orders(&self, _: &SymbolPair) -> Result, BoxError> { - let (response, _) = FutureRetry::new( - move || self.bfx.orders.active_orders(), - BitfinexConnector::handle_small_nonce_error, - ) - .await - .map_err(|(e, _attempts)| e)?; + let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?; Ok(response.iter().map(Into::into).collect()) } @@ -250,76 +309,17 @@ impl Connector for BitfinexConnector { BitfinexConnector::AFFILIATE_CODE.to_string(), )); - // retry to submit the order until it succeeds. - // the function may fail due to concurrent signed requests - // parsed in different times by the server - let response = { - loop { - match self.bfx.orders.submit_order(&order_form).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; - } - } - } - }; + let response = + BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?; Ok((&response).try_into()?) } - async fn order_book(&self, pair: &SymbolPair) -> Result { - let symbol_name = BitfinexConnector::format_trading_pair(pair); - - let response = { - loop { - match self - .bfx - .book - .trading_pair(symbol_name.clone(), bitfinex::book::BookPrecision::P0) - .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; - } - } - } - }; - - 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)) - } - async fn cancel_order(&self, order: &ActiveOrder) -> Result { let cancel_form = order.into(); - let response = { - loop { - match self.bfx.orders.cancel_order(&cancel_form).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; - } - } - } - }; + let response = + BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?; Ok((&response).try_into()?) } @@ -331,16 +331,48 @@ impl Connector for BitfinexConnector { symbol: Symbol, amount: f64, ) -> Result<(), BoxError> { - let result = self - .bfx - .account - .transfer_between_wallets(from.into(), to.into(), symbol.to_string(), amount) - .await?; - - println!("{:?}", result); + BitfinexConnector::retry_nonce(|| { + self.bfx.account.transfer_between_wallets( + from.into(), + to.into(), + symbol.to_string(), + amount, + ) + }) + .await?; Ok(()) } + + async fn trades_from_order( + &self, + order: &OrderDetails, + ) -> Result>, BoxError> { + let response = BitfinexConnector::retry_nonce(|| { + self.bfx + .trades + .generated_by_order(order.pair().trading_repr(), order.id()) + }) + .await?; + + if response.is_empty() { + Ok(None) + } else { + Ok(Some(response.iter().map(Into::into).collect())) + } + } + + async fn orders_history( + &self, + pair: &SymbolPair, + ) -> Result>, 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)) + } } impl From<&ActiveOrder> for CancelOrderForm { @@ -359,7 +391,7 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder { group_id: response.gid(), client_id: Some(response.cid()), symbol: SymbolPair::from_str(response.symbol())?, - current_form: OrderForm::new( + details: OrderForm::new( SymbolPair::from_str(response.symbol())?, response.into(), response.into(), @@ -557,7 +589,7 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder { group_id: order.group_id().map(|x| x as u64), client_id: Some(order.client_id()), symbol: pair.clone(), - current_form: OrderForm::new(pair, order.into(), order.into()), + details: OrderForm::new(pair, order.into(), order.into()), creation_timestamp: order.creation_timestamp(), update_timestamp: order.update_timestamp(), } @@ -590,3 +622,40 @@ impl From<&WalletKind> for &bitfinex::account::WalletKind { } } } + +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 { + let pair = SymbolPair::from_str(&response.symbol).unwrap(); + let fee = { + if response.is_maker { + OrderFee::Maker(response.fee) + } else { + OrderFee::Taker(response.fee) + } + }; + + Self { + trade_id: response.trade_id, + pair, + execution_timestamp: response.execution_timestamp, + price: response.execution_price, + amount: response.execution_amount, + fee, + fee_currency: Symbol::new(response.symbol.clone()), + } + } +} diff --git a/rustybot/src/currency.rs b/rustybot/src/currency.rs index 23ab15a..6f1d4a1 100644 --- a/rustybot/src/currency.rs +++ b/rustybot/src/currency.rs @@ -71,10 +71,10 @@ impl SymbolPair { SymbolPair { quote, base } } pub fn trading_repr(&self) -> String { - format!("t{}{}", self.quote, self.base) + format!("t{}{}", self.base, self.quote) } pub fn funding_repr(&self) -> String { - format!("f{}{}", self.quote, self.base) + format!("f{}{}", self.base, self.quote) } pub fn quote(&self) -> &Symbol { &self.quote diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index cc8485e..5b76e72 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -422,7 +422,7 @@ impl OrderManager { if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) { let opt_position_order = open_orders .iter() - .find(|x| x.current_form.amount().neg() == position.amount()); + .find(|x| x.details.amount().neg() == position.amount()); // checking if the position has an open order. // If so, don't do anything since the order is taken care of diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 42cfa87..1e137b5 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; use crate::connectors::Exchange; -use crate::currency::SymbolPair; +use crate::currency::{Symbol, SymbolPair}; /*************** * Prices @@ -104,6 +104,62 @@ impl OrderBook { } } +#[derive(Debug)] +pub enum OrderFee { + Maker(f64), + Taker(f64), +} + +#[derive(Debug)] +pub struct OrderDetails { + exchange: Exchange, + pair: SymbolPair, + platform: TradingPlatform, + kind: OrderKind, + execution_timestamp: u64, + id: u64, +} + +impl OrderDetails { + pub fn new( + exchange: Exchange, + id: u64, + pair: SymbolPair, + platform: TradingPlatform, + kind: OrderKind, + execution_timestamp: u64, + ) -> Self { + OrderDetails { + exchange, + pair, + platform, + kind, + execution_timestamp, + id, + } + } + + pub fn exchange(&self) -> Exchange { + self.exchange + } + pub fn platform(&self) -> TradingPlatform { + self.platform + } + pub fn kind(&self) -> OrderKind { + self.kind + } + pub fn execution_timestamp(&self) -> u64 { + self.execution_timestamp + } + pub fn id(&self) -> u64 { + self.id + } + + pub fn pair(&self) -> &SymbolPair { + &self.pair + } +} + #[derive(Clone, Debug)] pub struct ActiveOrder { pub(crate) exchange: Exchange, @@ -111,7 +167,7 @@ pub struct ActiveOrder { pub(crate) group_id: Option, pub(crate) client_id: Option, pub(crate) symbol: SymbolPair, - pub(crate) current_form: OrderForm, + pub(crate) details: OrderForm, pub(crate) creation_timestamp: u64, pub(crate) update_timestamp: u64, } @@ -496,3 +552,14 @@ pub enum WalletKind { Margin, Funding, } + +#[derive(Debug)] +pub struct Trade { + pub trade_id: u64, + pub pair: SymbolPair, + pub execution_timestamp: u64, + pub price: f64, + pub amount: f64, + pub fee: OrderFee, + pub fee_currency: Symbol, +} diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 8b88d48..df9a34a 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -75,9 +75,9 @@ pub struct TrailingStop { impl TrailingStop { const BREAK_EVEN_PERC: f64 = 0.1; - const MIN_PROFIT_PERC: f64 = 0.7; + const MIN_PROFIT_PERC: f64 = 0.5; const GOOD_PROFIT_PERC: f64 = TrailingStop::MIN_PROFIT_PERC * 1.75; - const MAX_LOSS_PERC: f64 = -1.75; + const MAX_LOSS_PERC: f64 = -4.0; pub fn new() -> Self { TrailingStop { @@ -276,7 +276,7 @@ impl OrderStrategy for FastOrderStrategy { // long let offer_comparison = { - if order.current_form.amount() > 0.0 { + if order.details.amount() > 0.0 { order_book.highest_bid() } else { order_book.lowest_ask() @@ -286,7 +286,7 @@ impl OrderStrategy for FastOrderStrategy { // if the best offer is higher than our threshold, // ask the manager to close the position with a market order let order_price = order - .current_form + .details .price() .ok_or("The active order does not have a price!")?; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; @@ -296,9 +296,9 @@ impl OrderStrategy for FastOrderStrategy { order: OrderForm::new( order.symbol.clone(), OrderKind::Market { - amount: order.current_form.amount(), + amount: order.details.amount(), }, - order.current_form.platform().clone(), + order.details.platform().clone(), ), }) }