use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; use log::{debug, info}; use tokio::sync::oneshot; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::{OrderManager, PositionManager, TrackedPositionsMap}; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, }; use crate::BoxError; /*************** * DEFINITIONS ***************/ pub trait PositionStrategy: DynClone + Send { fn name(&self) -> String; fn on_new_tick( &self, position: Position, manager: &PositionManager, ) -> (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 { /// 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_update(&self); /// This method is called when the OrderManager is requested to close /// a position that has an open order associated to it. fn on_position_close( &self, order: &ActiveOrder, open_position: &Position, order_book: &OrderBook, ) -> Result<(Option>, Option>), BoxError>; } 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 { const BREAK_EVEN_PERC: f64 = 0.2; const MIN_PROFIT_PERC: f64 = TrailingStop::BREAK_EVEN_PERC + 0.3; const GOOD_PROFIT_PERC: f64 = TrailingStop::MIN_PROFIT_PERC * 2.5; const MAX_LOSS_PERC: f64 = -0.01; const TAKER_FEE: f64 = 0.2; pub fn new() -> Self { TrailingStop { stop_percentages: HashMap::new(), } } fn net_pl_percentage(pl: f64, fee: f64) -> f64 { pl - fee } } impl PositionStrategy for TrailingStop { fn name(&self) -> String { "Trailing stop".into() } fn on_new_tick( &self, position: Position, manager: &PositionManager, ) -> (Position, Option>, Option>) { let mut messages = vec![]; let events = vec![]; let pl_perc = TrailingStop::net_pl_percentage(position.pl_perc(), TrailingStop::TAKER_FEE); 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 { debug!("Inserting close position message..."); messages.push(Message::ClosePosition { position: position.clone(), order_kind: OrderKind::Limit, }); PositionProfitState::Critical } }; let opt_pre_pw = manager.position_previous_tick(position.id(), None); let event_metadata = EventMetadata::new(Some(position.id()), None); let new_position = position.clone().with_profit_state(Some(state)); match opt_pre_pw { Some(prev) => { if prev.profit_state() == Some(state) { return ( new_position, (!events.is_empty()).then_some(events), (!messages.is_empty()).then_some(messages), ); } } None => { return ( new_position, (!events.is_empty()).then_some(events), (!messages.is_empty()).then_some(messages), ) } }; let events = { let mut events = vec![]; if state == PositionProfitState::Profit { events.push(Event::new( EventKind::ReachedGoodProfit, manager.current_tick(), Some(event_metadata), )); } else if state == PositionProfitState::MinimumProfit { events.push(Event::new( EventKind::ReachedMinProfit, manager.current_tick(), Some(event_metadata), )); } else if state == PositionProfitState::BreakEven { events.push(Event::new( EventKind::ReachedBreakEven, manager.current_tick(), Some(event_metadata), )); } else if state == PositionProfitState::Loss { events.push(Event::new( EventKind::ReachedLoss, manager.current_tick(), Some(event_metadata), )); } else { events.push(Event::new( EventKind::ReachedMaxLoss, manager.current_tick(), Some(event_metadata), )); } events }; return ( new_position, (!events.is_empty()).then_some(events), (!messages.is_empty()).then_some(messages), ); } } #[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.2 } } } impl FastOrderStrategy { pub fn new(threshold: f64) -> Self { Self { threshold } } } impl OrderStrategy for FastOrderStrategy { fn name(&self) -> String { "Fast order strategy".into() } fn on_update(&self) { unimplemented!() } fn on_position_close( &self, order: &ActiveOrder, active_position: &Position, order_book: &OrderBook, ) -> Result<(Option>, Option>), BoxError> { let mut messages = vec![]; // long let offer_comparison = { if order.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 delta = (1.0 - (offer_comparison / order.price)) * 100.0; debug!( "Offer comp: {} | Our offer: {} | Current delta: {}", offer_comparison, order.price, delta ); if delta > self.threshold { messages.push(Message::ClosePosition { position: active_position.clone(), order_kind: OrderKind::Market, }) } Ok((None, (!messages.is_empty()).then_some(messages))) } }