use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; use log::info; use crate::connectors::Connector; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::OptionUpdate; use crate::models::OrderBookEntry::Trading; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, PositionState, TradingPlatform, }; use crate::BoxError; /*************** * DEFINITIONS ***************/ pub trait PositionStrategy: DynClone + Send + Sync { fn name(&self) -> String; fn on_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, ) -> (Position, Option>, Option>); fn post_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, ) -> (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 { stop_percentages: HashMap, } impl TrailingStop { // in percentage const CAPITAL_MAX_LOSS: f64 = 17.5; const CAPITAL_MIN_PROFIT: f64 = 10.0; const CAPITAL_GOOD_PROFIT: f64 = 20.0; // in percentage const MIN_PROFIT_TRAILING_DELTA: f64 = 0.2; const GOOD_PROFIT_TRAILING_DELTA: f64 = 0.1; const LEVERAGE: f64 = 15.0; const MIN_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_MIN_PROFIT / TrailingStop::LEVERAGE) + TrailingStop::MIN_PROFIT_TRAILING_DELTA; const GOOD_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_GOOD_PROFIT / TrailingStop::LEVERAGE) + TrailingStop::GOOD_PROFIT_TRAILING_DELTA; const MAX_LOSS_PERC: f64 = -(TrailingStop::CAPITAL_MAX_LOSS / TrailingStop::LEVERAGE); pub fn new() -> Self { TrailingStop { stop_percentages: HashMap::new(), } } 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(TrailingStop::MIN_PROFIT_TRAILING_DELTA), PositionProfitState::Profit => Some(TrailingStop::GOOD_PROFIT_TRAILING_DELTA), _ => None, }; if let Some(profit_state_delta) = profit_state_delta { let current_stop_percentage = position.pl_perc() - profit_state_delta; match profit_state { PositionProfitState::MinimumProfit | PositionProfitState::Profit => { 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); } } } } _ => {} } } info!( "\tState: {:?} | PL%: {:0.2} | Stop: {:0.2}", position.profit_state().unwrap(), position.pl_perc(), self.stop_percentages.get(&position.id()).unwrap_or(&0.0) ); } } } impl PositionStrategy for TrailingStop { fn name(&self) -> String { "Trailing stop".into() } /// Sets the profit state of an open position fn on_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, ) -> (Position, Option>, Option>) { let pl_perc = position.pl_perc(); let state = { if pl_perc > TrailingStop::GOOD_PROFIT_PERC { PositionProfitState::Profit } else if TrailingStop::MIN_PROFIT_PERC <= pl_perc && pl_perc < TrailingStop::GOOD_PROFIT_PERC { PositionProfitState::MinimumProfit } else if 0.0 <= pl_perc && pl_perc < TrailingStop::MIN_PROFIT_PERC { PositionProfitState::BreakEven } else if TrailingStop::MAX_LOSS_PERC < pl_perc && pl_perc < 0.0 { 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.clone().with_profit_state(Some(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 events = { let mut events = vec![]; if state == PositionProfitState::Profit { events.push(Event::new( EventKind::ReachedGoodProfit, current_tick, Some(event_metadata), )); } else if state == PositionProfitState::MinimumProfit { events.push(Event::new( EventKind::ReachedMinProfit, current_tick, Some(event_metadata), )); } else if state == PositionProfitState::BreakEven { events.push(Event::new( EventKind::ReachedBreakEven, current_tick, Some(event_metadata), )); } else if state == PositionProfitState::Loss { events.push(Event::new( EventKind::ReachedLoss, current_tick, Some(event_metadata), )); } else { events.push(Event::new( EventKind::ReachedMaxLoss, current_tick, Some(event_metadata), )); } events }; return (new_position, Some(events), None); } fn post_tick( &mut self, position: Position, _: u64, _: &HashMap, ) -> (Position, Option>, Option>) { let close_message = Message::ClosePosition { position_id: position.id(), }; // if critical, early return with close position if let Some(profit_state) = position.profit_state() { match profit_state { PositionProfitState::Critical => { info!("Maximum loss reached. Closing position."); return (position, None, Some(vec![close_message])); } _ => {} } }; // 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])); } } self.update_stop_percentage(&position); (position, None, None) } } #[derive(Clone, Debug)] pub struct FastOrderStrategy { // threshold (%) for which we trigger a market order // to close an open position threshold: f64, } impl Default for FastOrderStrategy { fn default() -> Self { Self { threshold: 0.15 } } } impl FastOrderStrategy { pub fn new(threshold: f64) -> Self { Self { threshold } } } impl OrderStrategy for FastOrderStrategy { fn name(&self) -> String { "Fast order strategy".into() } fn on_open_order( &self, order: &ActiveOrder, order_book: &OrderBook, ) -> Result { let mut messages = vec![]; // long let offer_comparison = { if order.details.amount() > 0.0 { 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 .details .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(Message::SubmitOrder { order: OrderForm::new( order.symbol.clone(), OrderKind::Market { amount: order.details.amount(), }, order.details.platform().clone(), ), }) } Ok((None, (!messages.is_empty()).then_some(messages))) } // fn on_position_order( // &self, // order: &ActiveOrder, // _: &Position, // order_book: &OrderBook, // ) -> Result { // let mut messages = vec![]; // // // long // let offer_comparison = { // if order.current_form.amount() > 0.0 { // 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 // .current_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(Message::SubmitOrder { // order: OrderForm::new( // order.symbol.clone(), // OrderKind::Market { // amount: order.current_form.amount(), // }, // order.current_form.platform().clone(), // ), // }) // } // // Ok((None, (!messages.is_empty()).then_some(messages))) // } }