use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::ops::Neg; use dyn_clone::DynClone; use log::info; use crate::BoxError; use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::managers::OptionUpdate; use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees}; /*************** * DEFINITIONS ***************/ pub trait PositionStrategy: DynClone + Send + Sync { fn name(&self) -> String; fn on_tick( &mut self, 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>); } impl Debug for dyn PositionStrategy { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.name()) } } pub trait OrderStrategy: DynClone + Send + Sync { /// The name of the strategy, used for debugging purposes fn name(&self) -> String; /// This method is called when the OrderManager checks the open orders on a new tick. /// It should manage if some orders have to be closed or keep open. fn on_open_order( &self, order: &ActiveOrder, order_book: &OrderBook, ) -> Result; // /// This method is called when the OrderManager is requested to close // /// a position that has an open order associated to it. // fn on_position_order( // &self, // order: &ActiveOrder, // open_position: &Position, // order_book: &OrderBook, // ) -> Result; } impl Debug for dyn OrderStrategy { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.name()) } } /*************** * IMPLEMENTATIONS ***************/ #[derive(Clone, Debug)] pub struct TrailingStop { // Position ID: stop percentage mapping stop_percentages: HashMap, // Position ID: bool mapping. Represents when the strategy has asked the // order manager to set a stop loss order stop_loss_flags: HashMap, // Position ID: bool mapping. Represents when the strategy has asked the // order manager to set a limit order to close the position as the stop percentage // has been surpassed trail_set_flags: HashMap, capital_max_loss: f64, capital_min_profit: f64, capital_good_profit: f64, min_profit_trailing_delta: f64, good_profit_trailing_delta: f64, leverage: f64, min_profit_percentage: f64, good_profit_percentage: f64, max_loss_percentage: f64, } impl TrailingStop { fn print_status(&self, position: &Position) { match self.stop_percentages.get(&position.id()) { None => { info!( "\tState: {:?} | PL: {:0.2}{} ({:0.2}%)", position.profit_state().unwrap(), position.pl(), position.pair().quote(), position.pl_perc() ); } Some(stop_percentage) => { info!( "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", position.profit_state().unwrap(), position.pl(), position.pair().quote(), position.pl_perc(), stop_percentage ); } } } fn update_stop_percentage(&mut self, position: &Position) { if let Some(profit_state) = position.profit_state() { let profit_state_delta = match profit_state { PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta), PositionProfitState::Profit => Some(self.good_profit_trailing_delta), _ => None, }; if let Some(profit_state_delta) = profit_state_delta { let current_stop_percentage = position.pl_perc() - profit_state_delta; if let PositionProfitState::MinimumProfit | PositionProfitState::Profit = profit_state { match self.stop_percentages.get(&position.id()) { None => { self.stop_percentages .insert(position.id(), current_stop_percentage); } Some(existing_threshold) => { if existing_threshold < ¤t_stop_percentage { self.stop_percentages .insert(position.id(), current_stop_percentage); } } } } } } } } impl Default for TrailingStop { fn default() -> Self { let leverage = 15.0; // in percentage let capital_min_profit = 8.5; let capital_max_loss = capital_min_profit * 1.9; let capital_good_profit = capital_min_profit * 2.0; let weighted_min_profit = capital_min_profit / leverage; let weighted_good_profit = capital_good_profit / leverage; let weighted_max_loss = capital_max_loss / leverage; let min_profit_trailing_delta = weighted_min_profit * 0.17; let good_profit_trailing_delta = weighted_good_profit * 0.08; let min_profit_percentage = weighted_min_profit + min_profit_trailing_delta; let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta; let max_loss_percentage = -weighted_max_loss; TrailingStop { stop_percentages: Default::default(), stop_loss_flags: Default::default(), trail_set_flags: Default::default(), capital_max_loss, capital_min_profit, capital_good_profit, min_profit_trailing_delta, good_profit_trailing_delta, leverage, min_profit_percentage, good_profit_percentage, max_loss_percentage, } } } impl PositionStrategy for TrailingStop { fn name(&self) -> String { "Hidden Trailing Stop".into() } /// Sets the profit state of an open position fn on_tick( &mut self, 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 } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { PositionProfitState::MinimumProfit } else if (0.0..self.min_profit_percentage).contains(&pl_perc) { PositionProfitState::BreakEven } else if (self.max_loss_percentage..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical } }; let opt_prev_position = positions_history.get(&(current_tick - 1)); 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) { return (new_position, None, None); } } None => return (new_position, None, None), }; let event = match state { PositionProfitState::Critical => { Event::new( EventKind::ReachedMaxLoss, current_tick, ) } PositionProfitState::Loss => { Event::new( EventKind::ReachedLoss, current_tick, ) } PositionProfitState::BreakEven => { Event::new( EventKind::ReachedBreakEven, current_tick, ) } PositionProfitState::MinimumProfit => { Event::new( EventKind::ReachedMinProfit, current_tick, ) } PositionProfitState::Profit => { Event::new( EventKind::ReachedGoodProfit, current_tick, ) } }.with_metadata(Some(event_metadata)); (new_position, Some(vec![event]), None) } fn post_tick( &mut self, position: Position, _: u64, _: &HashMap, fees: &Vec, ) -> (Position, Option>, Option>) { 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); // we need to consider possible slippage when executing the stop order let slippage_percentage = self.max_loss_percentage * 0.085; // calculating the stop price based on short/long position let stop_loss_price = { if position.is_short() { position.base_price() * (1.0 - (self.max_loss_percentage - taker_fee - slippage_percentage) / 100.0) } else { position.base_price() * (1.0 + (self.max_loss_percentage - taker_fee - slippage_percentage) / 100.0) } }; let close_position_orders_msg = ActionMessage::ClosePositionOrders { position_id: position.id(), }; 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_leverage(Some(self.leverage)) .with_metadata(Some(OrderMetadata::new().with_position_id(Some(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); } self.update_stop_percentage(&position); self.print_status(&position); // 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."); messages.push(close_position_msg); return (position, None, Some(messages)); } } (position, None, Some(messages)) } } /* * ORDER STRATEGIES */ #[derive(Clone, Debug)] pub struct MarketEnforce { // threshold (%) for which we trigger a market order // to close an open position threshold: f64, } impl Default for MarketEnforce { fn default() -> Self { Self { threshold: 1.2 / 15.0, } } } impl OrderStrategy for MarketEnforce { fn name(&self) -> String { "Market Enforce".into() } fn on_open_order( &self, order: &ActiveOrder, order_book: &OrderBook, ) -> Result { let mut messages = vec![]; // long let offer_comparison = { if order.order_form().is_long() { order_book.highest_bid() } else { order_book.lowest_ask() } }; // if the best offer is higher than our threshold, // ask the manager to close the position with a market order let order_price = order .order_form() .price() .ok_or("The active order does not have a price!")?; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; if delta > self.threshold { messages.push(ActionMessage::SubmitOrder { order: OrderForm::new( order.pair().clone(), OrderKind::Market, *order.order_form().platform(), order.order_form().amount(), ) .with_leverage(order.order_form().leverage()) .with_metadata(order.order_form().metadata().clone()), }) } Ok((None, (!messages.is_empty()).then_some(messages))) } }