diff --git a/src/connectors.rs b/src/connectors.rs index 5b84ee7..ae268ec 100644 --- a/src/connectors.rs +++ b/src/connectors.rs @@ -14,12 +14,12 @@ use log::trace; use tokio::macros::support::Future; use tokio::time::Duration; +use crate::BoxError; use crate::currency::{Symbol, SymbolPair}; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind, }; -use crate::BoxError; #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum Exchange { @@ -164,6 +164,8 @@ impl Client { ) -> Result>, BoxError> { self.inner.orders_history(pair).await } + + pub async fn trading_fees(&self) -> Result, BoxError> { self.inner.trading_fees().await } } #[async_trait] @@ -183,7 +185,7 @@ pub trait Connector: Send + Sync { amount: f64, ) -> Result<(), BoxError>; async fn trades_from_order(&self, order: &OrderDetails) - -> Result>, BoxError>; + -> Result>, BoxError>; async fn orders_history( &self, pair: &SymbolPair, @@ -235,9 +237,9 @@ impl BitfinexConnector { // 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>, + where + F: FnMut() -> Fut, + Fut: Future>, { let response = { loop { @@ -291,7 +293,7 @@ impl Connector for BitfinexConnector { let response = BitfinexConnector::retry_nonce(|| { self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0) }) - .await?; + .await?; let entries = response .into_iter() @@ -327,7 +329,7 @@ impl Connector for BitfinexConnector { OrderKind::Stop { price } => { bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) } - OrderKind::StopLimit { price, limit_price } => { + OrderKind::StopLimit { stop_price: price, limit_price } => { bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) .with_price_aux_limit(limit_price)? } @@ -342,9 +344,9 @@ impl Connector for BitfinexConnector { bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) } } - .with_meta(OrderMeta::new( - BitfinexConnector::AFFILIATE_CODE.to_string(), - )) + .with_meta(OrderMeta::new( + BitfinexConnector::AFFILIATE_CODE.to_string(), + )) }; // adding leverage, if any @@ -386,7 +388,7 @@ impl Connector for BitfinexConnector { amount, ) }) - .await?; + .await?; Ok(()) } @@ -400,7 +402,7 @@ impl Connector for BitfinexConnector { .trades .generated_by_order(order.pair().trading_repr(), order.id()) }) - .await?; + .await?; if response.is_empty() { Ok(None) @@ -486,8 +488,8 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder { response.mts_create(), response.mts_update(), ) - .with_group_id(response.gid()) - .with_client_id(Some(response.cid()))) + .with_group_id(response.gid()) + .with_client_id(Some(response.cid()))) } } @@ -524,8 +526,8 @@ impl TryInto for bitfinex::positions::Position { platform, self.leverage(), ) - .with_creation_date(self.mts_create()) - .with_creation_update(self.mts_update())) + .with_creation_date(self.mts_create()) + .with_creation_update(self.mts_update())) } } @@ -603,7 +605,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind { } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { - price: response.price(), + stop_price: response.price(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop @@ -642,7 +644,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind { } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { - price: response.price(), + stop_price: response.price(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop @@ -675,8 +677,8 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder { order.creation_timestamp(), order.update_timestamp(), ) - .with_client_id(Some(order.client_id())) - .with_group_id(order.group_id()) + .with_client_id(Some(order.client_id())) + .with_group_id(order.group_id()) } } diff --git a/src/managers.rs b/src/managers.rs index e2ceee2..3e2ddc5 100644 --- a/src/managers.rs +++ b/src/managers.rs @@ -5,11 +5,12 @@ use futures_util::stream::FuturesUnordered; use futures_util::StreamExt; use log::{debug, error, info, trace}; use merge::Merge; -use tokio::sync::mpsc::channel; use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::mpsc::channel; use tokio::sync::oneshot; use tokio::time::Duration; +use crate::BoxError; use crate::connectors::{Client, ExchangeDetails}; use crate::currency::SymbolPair; use crate::events::{ActionMessage, ActorMessage, Event}; @@ -17,7 +18,6 @@ use crate::models::{ ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, }; use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; -use crate::BoxError; pub type OptionUpdate = (Option>, Option>); @@ -234,8 +234,10 @@ impl PositionManager { pub async fn update(&mut self, tick: u64) -> Result { trace!("\t[PositionManager] Updating {}", self.pair); - let opt_active_positions = self.client.active_positions(&self.pair).await?; self.current_tick = tick; + let (fees, opt_active_positions) = tokio::join!(self.client.trading_fees(),self.client.active_positions(&self.pair)); + let (fees, opt_active_positions) = (fees?, opt_active_positions?); + // we assume there is only ONE active position per pair match opt_active_positions { @@ -258,11 +260,11 @@ impl PositionManager { let (pos_on_tick, events_on_tick, messages_on_tick) = self .strategy - .on_tick(position, self.current_tick(), &self.positions_history); + .on_tick(position, self.current_tick(), &self.positions_history, &fees); let (pos_post_tick, events_post_tick, messages_post_tick) = self .strategy - .post_tick(pos_on_tick, self.current_tick(), &self.positions_history); + .post_tick(pos_on_tick, self.current_tick(), &self.positions_history, &fees); events.merge(events_on_tick); events.merge(events_post_tick); @@ -513,7 +515,7 @@ impl OrderManager { position.platform(), position.amount().neg(), ) - .with_leverage(Some(position.leverage())); + .with_leverage(Some(position.leverage())); info!("Submitting {} order", order_form.kind()); if let Err(e) = self.client.submit_order(&order_form).await { diff --git a/src/strategy.rs b/src/strategy.rs index 521142c..fbf1d12 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; +use std::ops::Neg; use dyn_clone::DynClone; use log::info; @@ -8,7 +9,7 @@ use crate::BoxError; use crate::connectors::Connector; use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::managers::OptionUpdate; -use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; +use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees}; /*************** * DEFINITIONS @@ -21,12 +22,14 @@ pub trait PositionStrategy: DynClone + Send + Sync { position: Position, current_tick: u64, positions_history: &HashMap, + fees: &Vec, ) -> (Position, Option>, Option>); fn post_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, + fees: &Vec, ) -> (Position, Option>, Option>); } @@ -68,7 +71,10 @@ impl Debug for dyn OrderStrategy { #[derive(Clone, Debug)] pub struct HiddenTrailingStop { + // position id: stop_percentage stop_percentages: HashMap, + // position_id: bool + stop_loss_flags: HashMap, capital_max_loss: f64, capital_min_profit: f64, capital_good_profit: f64, @@ -80,6 +86,7 @@ pub struct HiddenTrailingStop { max_loss_percentage: f64, } + impl HiddenTrailingStop { fn update_stop_percentage(&mut self, position: &Position) { if let Some(profit_state) = position.profit_state() { @@ -144,6 +151,7 @@ impl Default for HiddenTrailingStop { HiddenTrailingStop { stop_percentages: Default::default(), + stop_loss_flags: Default::default(), capital_max_loss, capital_min_profit, capital_good_profit, @@ -168,9 +176,11 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, current_tick: u64, positions_history: &HashMap, + _: &Vec, ) -> (Position, Option>, Option>) { let pl_perc = position.pl_perc(); + // setting the state of the position based on its profit/loss percentage let state = { if pl_perc > self.good_profit_percentage { PositionProfitState::Profit @@ -189,6 +199,8 @@ impl PositionStrategy for HiddenTrailingStop { let event_metadata = EventMetadata::new(Some(position.id()), None); let new_position = position.with_profit_state(Some(state)); + // checking if there was a state change between the current position + // and its last state match opt_prev_position { Some(prev) => { if prev.profit_state() == Some(state) { @@ -239,28 +251,75 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, _: u64, _: &HashMap, + fees: &Vec, ) -> (Position, Option>, Option>) { - let close_message = ActionMessage::ClosePosition { + let taker_fee = fees + .iter() + .filter_map(|x| match x { + TradingFees::Taker { + platform, + percentage, + } if platform == &position.platform() => Some(percentage), + _ => None, + }) + .next().map_or_else(|| 0.0, |&x| x); + + let stop_loss_price = { + if position.is_short() { + position.base_price() * (1.0 - (self.max_loss_percentage - taker_fee) / 100.0) + } else { + position.base_price() * (1.0 + (self.max_loss_percentage - taker_fee) / 100.0) + } + }; + let close_position_orders_msg = ActionMessage::ClosePositionOrders { position_id: position.id(), }; - - // if critical, early return with close position - if let Some(PositionProfitState::Critical) = position.profit_state() { - info!("Maximum loss reached. Closing position."); - return (position, None, Some(vec![close_message])); + let close_position_msg = ActionMessage::ClosePosition { + position_id: position.id(), }; + let set_stop_loss_msg = ActionMessage::SubmitOrder { + order: OrderForm::new(position.pair().clone(), + OrderKind::Stop { price: stop_loss_price }, + position.platform(), + position.amount().neg()) + .with_metadata(Some(OrderMetadata::with_position_id(position.id()))) + }; + let stop_loss_set = *self.stop_loss_flags.entry(position.id()).or_insert(false); + + // if in loss, ask the order manager to set the stop limit order, + // if not already set + if let Some(PositionProfitState::Critical) | Some(PositionProfitState::Loss) = position.profit_state() { + if !stop_loss_set { + info!("In loss. Opening trailing stop order."); + + self.stop_loss_flags.insert(position.id(), true); + return (position, None, Some(vec![set_stop_loss_msg])); + } + return (position, None, None); + } + + let mut messages = vec![]; + + // if a stop loss order was previously set, + // ask the order manager to remove the order first + if stop_loss_set { + info!("Removing stop loss order."); + messages.push(close_position_orders_msg); + self.stop_loss_flags.insert(position.id(), false); + } // let's check if we surpassed an existing stop percentage if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) { if &position.pl_perc() <= existing_stop_percentage { info!("Stop percentage surpassed. Closing position."); - return (position, None, Some(vec![close_message])); + messages.push(close_position_msg); + return (position, None, Some(messages)); } } self.update_stop_percentage(&position); - (position, None, None) + (position, None, Some(messages)) } } @@ -531,7 +590,7 @@ pub struct MarketEnforce { impl Default for MarketEnforce { fn default() -> Self { Self { - threshold: 1.0 / 15.0, + threshold: 100.0, } } }