diff --git a/rustybot/src/bot.rs b/rustybot/src/bot.rs index bea705a..3d48ae5 100644 --- a/rustybot/src/bot.rs +++ b/rustybot/src/bot.rs @@ -1,12 +1,13 @@ use core::time::Duration; -use log::{error, info}; +use log::{debug, error, info}; use tokio::time::delay_for; use crate::connectors::{Client, ExchangeKind}; use crate::currency::{Symbol, SymbolPair}; use crate::events::Event; use crate::managers::{OrderManager, PositionManager, PriceManager}; +use crate::strategy::PositionStrategy; use crate::ticker::Ticker; use crate::BoxError; @@ -54,6 +55,16 @@ impl BfxBot { } } + pub fn with_position_strategy(mut self, strategy: Box) -> Self { + self.pos_managers = self + .pos_managers + .into_iter() + .map(|x| x.with_strategy(dyn_clone::clone_box(&*strategy))) + .collect(); + + self + } + pub async fn start_loop(&mut self) -> Result<(), BoxError> { if let Err(e) = self.update_managers().await { error!("Error while starting managers: {}", e); diff --git a/rustybot/src/main.rs b/rustybot/src/main.rs index 57b3e00..a280619 100644 --- a/rustybot/src/main.rs +++ b/rustybot/src/main.rs @@ -7,6 +7,7 @@ use tokio::time::Duration; use crate::bot::BfxBot; use crate::connectors::ExchangeKind; use crate::currency::Symbol; +use crate::strategy::TrailingStop; mod bot; mod connectors; @@ -38,7 +39,8 @@ async fn main() -> Result<(), BoxError> { vec![Symbol::TESTBTC], Symbol::TESTUSD, Duration::new(1, 0), - ); + ) + .with_position_strategy(Box::new(TrailingStop::new())); Ok(bot.start_loop().await?) } diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index 9db42d6..4f28192 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -84,23 +84,6 @@ impl PriceManager { pub fn pair(&self) -> &SymbolPair { &self.pair } - - // pub fn position_previous_tick(&self, id: u64, tick: Option) -> Option<&Position> { - // let tick = match tick { - // Some(tick) => { - // if tick < 1 { - // 1 - // } else { - // tick - // } - // } - // None => self.current_tick() - 1, - // }; - // - // self.positions - // .get(&tick) - // .and_then(|x| x.iter().find(|x| x.position_id() == id)) - // } } #[derive(Clone, Debug)] @@ -148,6 +131,7 @@ impl PriceEntry { #[derive(Debug)] pub struct PositionManager { + current_tick: u64, pair: SymbolPair, positions_history: HashMap, active_position: Option, @@ -158,6 +142,7 @@ pub struct PositionManager { impl PositionManager { pub fn new(pair: SymbolPair, client: Client) -> Self { PositionManager { + current_tick: 0, pair, positions_history: HashMap::new(), active_position: None, @@ -171,10 +156,16 @@ impl PositionManager { self } + pub fn current_tick(&self) -> u64 { + self.current_tick + } + pub async fn update(&mut self, tick: u64) -> Result>, BoxError> { let opt_active_positions = self.client.active_positions(&self.pair).await?; let mut events = vec![]; + self.current_tick = tick; + if opt_active_positions.is_none() { return Ok(None); } @@ -200,7 +191,8 @@ impl PositionManager { } }; - self.positions_history.insert(tick, active_position.clone()); + self.positions_history + .insert(self.current_tick(), active_position.clone()); self.active_position = Some(active_position); } None => { @@ -214,6 +206,24 @@ impl PositionManager { Ok(Some(events)) } } + + pub fn position_previous_tick(&self, id: u64, tick: Option) -> Option<&Position> { + let tick = match tick { + Some(tick) => { + if tick < 1 { + 1 + } else { + tick + } + } + None => self.current_tick() - 1, + }; + + self.positions_history + .get(&tick) + .filter(|x| x.position_id() == id) + .and_then(|x| Some(x)) + } } pub struct OrderManager { diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index a6fe086..bdf1fa6 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -1,10 +1,11 @@ +use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; -use crate::events::{Event, SignalKind}; +use crate::events::{Event, EventKind, EventMetadata, SignalKind}; use crate::managers::PositionManager; -use crate::models::Position; +use crate::models::{Position, PositionProfitState}; pub trait PositionStrategy: DynClone { fn on_new_tick( @@ -20,110 +21,110 @@ impl Debug for dyn PositionStrategy { } } -// #[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 = -1.7; -// -// 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 on_new_tick( -// &self, -// position: &Position, -// status: &PairStatus, -// ) -> (Position, Vec, Vec) { -// let mut signals = vec![]; -// let pl_perc = TrailingStop::net_pl_percentage(position.pl_perc(), TrailingStop::TAKER_FEE); -// let events = vec![]; -// -// 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 { -// signals.push(SignalKind::ClosePosition { -// position_id: position.position_id(), -// }); -// PositionProfitState::Critical -// } -// }; -// -// let opt_pre_pw = status.position_previous_tick(position.position_id(), None); -// let event_metadata = EventMetadata::new(Some(position.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, signals); -// } -// } -// None => return (new_position, events, signals), -// }; -// -// let events = { -// let mut events = vec![]; -// -// if state == PositionProfitState::Profit { -// events.push(Event::new( -// EventKind::ReachedGoodProfit, -// status.current_tick(), -// Some(event_metadata), -// )); -// } else if state == PositionProfitState::MinimumProfit { -// events.push(Event::new( -// EventKind::ReachedMinProfit, -// status.current_tick(), -// Some(event_metadata), -// )); -// } else if state == PositionProfitState::BreakEven { -// events.push(Event::new( -// EventKind::ReachedBreakEven, -// status.current_tick(), -// Some(event_metadata), -// )); -// } else if state == PositionProfitState::Loss { -// events.push(Event::new( -// EventKind::ReachedLoss, -// status.current_tick(), -// Some(event_metadata), -// )); -// } else { -// events.push(Event::new( -// EventKind::ReachedMaxLoss, -// status.current_tick(), -// Some(event_metadata), -// )); -// } -// -// events -// }; -// -// return (new_position, events, signals); -// } -// } +#[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 = -1.7; + + 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 on_new_tick( + &self, + position: &Position, + manager: &PositionManager, + ) -> (Position, Vec, Vec) { + let mut signals = vec![]; + let pl_perc = TrailingStop::net_pl_percentage(position.pl_perc(), TrailingStop::TAKER_FEE); + let events = vec![]; + + 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 { + signals.push(SignalKind::ClosePosition { + position_id: position.position_id(), + }); + PositionProfitState::Critical + } + }; + + let opt_pre_pw = manager.position_previous_tick(position.position_id(), None); + let event_metadata = EventMetadata::new(Some(position.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, signals); + } + } + None => return (new_position, events, signals), + }; + + 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, signals); + } +}