use dyn_clone::DynClone; use log::debug; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::{OrderManager, PositionManager, TrackedPositionsMap}; use crate::models::{ActiveOrder, OrderForm, Position, PositionProfitState}; use tokio::sync::oneshot; /*************** * 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, tracked_positions: &HashMap, ) -> TrackedPositionsMap; } 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(), }); 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 {} impl OrderStrategy for FastOrderStrategy { fn name(&self) -> String { "Fast order strategy".into() } fn on_update(&self) { unimplemented!() } fn on_position_close( &self, order: &ActiveOrder, tracked_positions: &HashMap, ) -> TrackedPositionsMap { unimplemented!() } }