implemented trades from orders and orders history

This commit is contained in:
Giulio De Pasquale 2021-01-28 20:06:11 +00:00
parent c930dce131
commit d383328ebb
5 changed files with 234 additions and 98 deletions

View File

@ -5,20 +5,23 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bitfinex::api::Bitfinex; use bitfinex::api::Bitfinex;
use bitfinex::book::{Book, BookPrecision};
use bitfinex::orders::{CancelOrderForm, OrderMeta}; use bitfinex::orders::{CancelOrderForm, OrderMeta};
use bitfinex::responses::{OrderResponse, TradeResponse};
use bitfinex::ticker::TradingPairTicker; use bitfinex::ticker::TradingPairTicker;
use futures_retry::{FutureRetry, RetryPolicy}; use futures_retry::{FutureRetry, RetryPolicy};
use log::trace; use log::trace;
use tokio::macros::support::Future; use tokio::macros::support::Future;
use tokio::time::Duration; use tokio::time::Duration;
use tokio_tungstenite::stream::Stream::Plain;
use crate::currency::{Symbol, SymbolPair}; use crate::currency::{Symbol, SymbolPair};
use crate::models::TradingPlatform::Margin;
use crate::models::{ use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionState, ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
PriceTicker, TradingPlatform, WalletKind, PositionState, PriceTicker, Trade, TradingPlatform, WalletKind,
}; };
use crate::BoxError; use crate::BoxError;
use bitfinex::responses::OrderResponse;
#[derive(PartialEq, Eq, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Exchange { pub enum Exchange {
@ -114,6 +117,20 @@ impl Client {
.transfer_between_wallets(from, to, symbol, amount) .transfer_between_wallets(from, to, symbol, amount)
.await .await
} }
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
}
} }
#[async_trait] #[async_trait]
@ -132,6 +149,12 @@ pub trait Connector: Send + Sync {
symbol: Symbol, symbol: Symbol,
amount: f64, amount: f64,
) -> Result<(), BoxError>; ) -> Result<(), BoxError>;
async fn trades_from_order(&self, order: &OrderDetails)
-> Result<Option<Vec<Trade>>, BoxError>;
async fn orders_history(
&self,
pair: &SymbolPair,
) -> Result<Option<Vec<OrderDetails>>, BoxError>;
} }
impl Debug for dyn Connector { impl Debug for dyn Connector {
@ -171,6 +194,31 @@ impl BitfinexConnector {
format!("{}{}", pair.base(), pair.quote()) 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<F, Fut, O>(mut func: F) -> Result<O, BoxError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<O, BoxError>>,
{
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] #[async_trait]
@ -180,12 +228,8 @@ impl Connector for BitfinexConnector {
} }
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> { async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
let (active_positions, _) = FutureRetry::new( let active_positions =
move || self.bfx.positions.active_positions(), BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?;
BitfinexConnector::handle_small_nonce_error,
)
.await
.map_err(|(e, _attempts)| e)?;
let positions: Vec<_> = active_positions let positions: Vec<_> = active_positions
.into_iter() .into_iter()
@ -205,13 +249,28 @@ impl Connector for BitfinexConnector {
Ok(ticker) Ok(ticker)
} }
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)
})
.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<Vec<ActiveOrder>, BoxError> { async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
let (response, _) = FutureRetry::new( let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?;
move || self.bfx.orders.active_orders(),
BitfinexConnector::handle_small_nonce_error,
)
.await
.map_err(|(e, _attempts)| e)?;
Ok(response.iter().map(Into::into).collect()) Ok(response.iter().map(Into::into).collect())
} }
@ -250,76 +309,17 @@ impl Connector for BitfinexConnector {
BitfinexConnector::AFFILIATE_CODE.to_string(), BitfinexConnector::AFFILIATE_CODE.to_string(),
)); ));
// retry to submit the order until it succeeds. let response =
// the function may fail due to concurrent signed requests BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?;
// 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;
}
}
}
};
Ok((&response).try_into()?) Ok((&response).try_into()?)
} }
async fn order_book(&self, pair: &SymbolPair) -> Result<OrderBook, BoxError> {
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<ActiveOrder, BoxError> { async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
let cancel_form = order.into(); let cancel_form = order.into();
let response = { let response =
loop { BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?;
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;
}
}
}
};
Ok((&response).try_into()?) Ok((&response).try_into()?)
} }
@ -331,16 +331,48 @@ impl Connector for BitfinexConnector {
symbol: Symbol, symbol: Symbol,
amount: f64, amount: f64,
) -> Result<(), BoxError> { ) -> Result<(), BoxError> {
let result = self BitfinexConnector::retry_nonce(|| {
.bfx self.bfx.account.transfer_between_wallets(
.account from.into(),
.transfer_between_wallets(from.into(), to.into(), symbol.to_string(), amount) to.into(),
.await?; symbol.to_string(),
amount,
println!("{:?}", result); )
})
.await?;
Ok(()) Ok(())
} }
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())
})
.await?;
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))
}
} }
impl From<&ActiveOrder> for CancelOrderForm { impl From<&ActiveOrder> for CancelOrderForm {
@ -359,7 +391,7 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder {
group_id: response.gid(), group_id: response.gid(),
client_id: Some(response.cid()), client_id: Some(response.cid()),
symbol: SymbolPair::from_str(response.symbol())?, symbol: SymbolPair::from_str(response.symbol())?,
current_form: OrderForm::new( details: OrderForm::new(
SymbolPair::from_str(response.symbol())?, SymbolPair::from_str(response.symbol())?,
response.into(), response.into(),
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), group_id: order.group_id().map(|x| x as u64),
client_id: Some(order.client_id()), client_id: Some(order.client_id()),
symbol: pair.clone(), 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(), creation_timestamp: order.creation_timestamp(),
update_timestamp: order.update_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()),
}
}
}

View File

@ -71,10 +71,10 @@ impl SymbolPair {
SymbolPair { quote, base } SymbolPair { quote, base }
} }
pub fn trading_repr(&self) -> String { pub fn trading_repr(&self) -> String {
format!("t{}{}", self.quote, self.base) format!("t{}{}", self.base, self.quote)
} }
pub fn funding_repr(&self) -> String { pub fn funding_repr(&self) -> String {
format!("f{}{}", self.quote, self.base) format!("f{}{}", self.base, self.quote)
} }
pub fn quote(&self) -> &Symbol { pub fn quote(&self) -> &Symbol {
&self.quote &self.quote

View File

@ -422,7 +422,7 @@ impl OrderManager {
if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) { if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) {
let opt_position_order = open_orders let opt_position_order = open_orders
.iter() .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. // checking if the position has an open order.
// If so, don't do anything since the order is taken care of // If so, don't do anything since the order is taken care of

View File

@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use crate::connectors::Exchange; use crate::connectors::Exchange;
use crate::currency::SymbolPair; use crate::currency::{Symbol, SymbolPair};
/*************** /***************
* Prices * 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)] #[derive(Clone, Debug)]
pub struct ActiveOrder { pub struct ActiveOrder {
pub(crate) exchange: Exchange, pub(crate) exchange: Exchange,
@ -111,7 +167,7 @@ pub struct ActiveOrder {
pub(crate) group_id: Option<u64>, pub(crate) group_id: Option<u64>,
pub(crate) client_id: Option<u64>, pub(crate) client_id: Option<u64>,
pub(crate) symbol: SymbolPair, pub(crate) symbol: SymbolPair,
pub(crate) current_form: OrderForm, pub(crate) details: OrderForm,
pub(crate) creation_timestamp: u64, pub(crate) creation_timestamp: u64,
pub(crate) update_timestamp: u64, pub(crate) update_timestamp: u64,
} }
@ -496,3 +552,14 @@ pub enum WalletKind {
Margin, Margin,
Funding, 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,
}

View File

@ -75,9 +75,9 @@ pub struct TrailingStop {
impl TrailingStop { impl TrailingStop {
const BREAK_EVEN_PERC: f64 = 0.1; 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 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 { pub fn new() -> Self {
TrailingStop { TrailingStop {
@ -276,7 +276,7 @@ impl OrderStrategy for FastOrderStrategy {
// long // long
let offer_comparison = { let offer_comparison = {
if order.current_form.amount() > 0.0 { if order.details.amount() > 0.0 {
order_book.highest_bid() order_book.highest_bid()
} else { } else {
order_book.lowest_ask() order_book.lowest_ask()
@ -286,7 +286,7 @@ impl OrderStrategy for FastOrderStrategy {
// if the best offer is higher than our threshold, // if the best offer is higher than our threshold,
// ask the manager to close the position with a market order // ask the manager to close the position with a market order
let order_price = order let order_price = order
.current_form .details
.price() .price()
.ok_or("The active order does not have a price!")?; .ok_or("The active order does not have a price!")?;
let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0;
@ -296,9 +296,9 @@ impl OrderStrategy for FastOrderStrategy {
order: OrderForm::new( order: OrderForm::new(
order.symbol.clone(), order.symbol.clone(),
OrderKind::Market { OrderKind::Market {
amount: order.current_form.amount(), amount: order.details.amount(),
}, },
order.current_form.platform().clone(), order.details.platform().clone(),
), ),
}) })
} }