implemented stop order for trailing stop. position strategy now receives fees information. added fees API to client.

This commit is contained in:
Giulio De Pasquale 2021-02-20 21:01:32 +00:00
parent 386137f16e
commit 2d81358fa0
3 changed files with 99 additions and 36 deletions

View File

@ -14,12 +14,12 @@ use log::trace;
use tokio::macros::support::Future; use tokio::macros::support::Future;
use tokio::time::Duration; use tokio::time::Duration;
use crate::BoxError;
use crate::currency::{Symbol, SymbolPair}; use crate::currency::{Symbol, SymbolPair};
use crate::models::{ use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind, PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
}; };
use crate::BoxError;
#[derive(PartialEq, Eq, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Exchange { pub enum Exchange {
@ -164,6 +164,8 @@ impl Client {
) -> Result<Option<Vec<OrderDetails>>, BoxError> { ) -> Result<Option<Vec<OrderDetails>>, BoxError> {
self.inner.orders_history(pair).await self.inner.orders_history(pair).await
} }
pub async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { self.inner.trading_fees().await }
} }
#[async_trait] #[async_trait]
@ -237,7 +239,7 @@ impl BitfinexConnector {
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError> async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
where where
F: FnMut() -> Fut, F: FnMut() -> Fut,
Fut: Future<Output = Result<O, BoxError>>, Fut: Future<Output=Result<O, BoxError>>,
{ {
let response = { let response = {
loop { loop {
@ -327,7 +329,7 @@ impl Connector for BitfinexConnector {
OrderKind::Stop { price } => { OrderKind::Stop { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) 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()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
.with_price_aux_limit(limit_price)? .with_price_aux_limit(limit_price)?
} }
@ -603,7 +605,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::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!"), limit_price: response.price_aux_limit().expect("Limit price not found!"),
}, },
bitfinex::orders::OrderKind::TrailingStop bitfinex::orders::OrderKind::TrailingStop
@ -642,7 +644,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::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!"), limit_price: response.price_aux_limit().expect("Limit price not found!"),
}, },
bitfinex::orders::OrderKind::TrailingStop bitfinex::orders::OrderKind::TrailingStop

View File

@ -5,11 +5,12 @@ use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
use merge::Merge; use merge::Merge;
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::mpsc::channel;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::time::Duration; use tokio::time::Duration;
use crate::BoxError;
use crate::connectors::{Client, ExchangeDetails}; use crate::connectors::{Client, ExchangeDetails};
use crate::currency::SymbolPair; use crate::currency::SymbolPair;
use crate::events::{ActionMessage, ActorMessage, Event}; use crate::events::{ActionMessage, ActorMessage, Event};
@ -17,7 +18,6 @@ use crate::models::{
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker,
}; };
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
use crate::BoxError;
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>); pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
@ -234,8 +234,10 @@ impl PositionManager {
pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> { pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> {
trace!("\t[PositionManager] Updating {}", self.pair); trace!("\t[PositionManager] Updating {}", self.pair);
let opt_active_positions = self.client.active_positions(&self.pair).await?;
self.current_tick = tick; 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 // we assume there is only ONE active position per pair
match opt_active_positions { match opt_active_positions {
@ -258,11 +260,11 @@ impl PositionManager {
let (pos_on_tick, events_on_tick, messages_on_tick) = self let (pos_on_tick, events_on_tick, messages_on_tick) = self
.strategy .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 let (pos_post_tick, events_post_tick, messages_post_tick) = self
.strategy .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_on_tick);
events.merge(events_post_tick); events.merge(events_post_tick);

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::ops::Neg;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use log::info; use log::info;
@ -8,7 +9,7 @@ use crate::BoxError;
use crate::connectors::Connector; use crate::connectors::Connector;
use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
use crate::managers::OptionUpdate; 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 * DEFINITIONS
@ -21,12 +22,14 @@ pub trait PositionStrategy: DynClone + Send + Sync {
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>); ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
fn post_tick( fn post_tick(
&mut self, &mut self,
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>); ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
} }
@ -68,7 +71,10 @@ impl Debug for dyn OrderStrategy {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HiddenTrailingStop { pub struct HiddenTrailingStop {
// position id: stop_percentage
stop_percentages: HashMap<u64, f64>, stop_percentages: HashMap<u64, f64>,
// position_id: bool
stop_loss_flags: HashMap<u64, bool>,
capital_max_loss: f64, capital_max_loss: f64,
capital_min_profit: f64, capital_min_profit: f64,
capital_good_profit: f64, capital_good_profit: f64,
@ -80,6 +86,7 @@ pub struct HiddenTrailingStop {
max_loss_percentage: f64, max_loss_percentage: f64,
} }
impl HiddenTrailingStop { impl HiddenTrailingStop {
fn update_stop_percentage(&mut self, position: &Position) { fn update_stop_percentage(&mut self, position: &Position) {
if let Some(profit_state) = position.profit_state() { if let Some(profit_state) = position.profit_state() {
@ -144,6 +151,7 @@ impl Default for HiddenTrailingStop {
HiddenTrailingStop { HiddenTrailingStop {
stop_percentages: Default::default(), stop_percentages: Default::default(),
stop_loss_flags: Default::default(),
capital_max_loss, capital_max_loss,
capital_min_profit, capital_min_profit,
capital_good_profit, capital_good_profit,
@ -168,9 +176,11 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
_: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let pl_perc = position.pl_perc(); let pl_perc = position.pl_perc();
// setting the state of the position based on its profit/loss percentage
let state = { let state = {
if pl_perc > self.good_profit_percentage { if pl_perc > self.good_profit_percentage {
PositionProfitState::Profit PositionProfitState::Profit
@ -189,6 +199,8 @@ impl PositionStrategy for HiddenTrailingStop {
let event_metadata = EventMetadata::new(Some(position.id()), None); let event_metadata = EventMetadata::new(Some(position.id()), None);
let new_position = position.with_profit_state(Some(state)); 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 { match opt_prev_position {
Some(prev) => { Some(prev) => {
if prev.profit_state() == Some(state) { if prev.profit_state() == Some(state) {
@ -239,28 +251,75 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position, position: Position,
_: u64, _: u64,
_: &HashMap<u64, Position>, _: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
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(), position_id: position.id(),
}; };
let close_position_msg = ActionMessage::ClosePosition {
// if critical, early return with close position position_id: position.id(),
if let Some(PositionProfitState::Critical) = position.profit_state() {
info!("Maximum loss reached. Closing position.");
return (position, None, Some(vec![close_message]));
}; };
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 // let's check if we surpassed an existing stop percentage
if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) { if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
if &position.pl_perc() <= existing_stop_percentage { if &position.pl_perc() <= existing_stop_percentage {
info!("Stop percentage surpassed. Closing position."); 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); self.update_stop_percentage(&position);
(position, None, None) (position, None, Some(messages))
} }
} }
@ -531,7 +590,7 @@ pub struct MarketEnforce {
impl Default for MarketEnforce { impl Default for MarketEnforce {
fn default() -> Self { fn default() -> Self {
Self { Self {
threshold: 1.0 / 15.0, threshold: 100.0,
} }
} }
} }