diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index ed32db7..0d828c0 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -12,14 +12,14 @@ use tokio::time::Duration; use crate::connectors::{Client, ExchangeDetails}; use crate::currency::SymbolPair; -use crate::events::{ActorMessage, Event, Message}; +use crate::events::{ActionMessage, ActorMessage, Event}; use crate::models::{ ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, TradingPlatform, }; use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::BoxError; -pub type OptionUpdate = (Option>, Option>); +pub type OptionUpdate = (Option>, Option>); /****************** * PRICES @@ -44,7 +44,7 @@ impl PriceManager { } pub async fn handle_message(&mut self, message: ActorMessage) -> Result<(), BoxError> { - if let Message::Update { tick } = message.message { + if let ActionMessage::Update { tick } = message.message { let a = self.update(tick).await?; self.add_entry(a); } @@ -100,7 +100,7 @@ impl PriceManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -176,7 +176,7 @@ impl PositionManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -221,7 +221,7 @@ impl PositionManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { tick } => self.update(tick).await?, + ActionMessage::Update { tick } => self.update(tick).await?, _ => (None, None), }; @@ -302,7 +302,7 @@ impl PositionManager { ******************/ // Position ID: Order ID -pub type TrackedPositionsMap = HashMap; +pub type TrackedPositionsMap = HashMap>; pub struct OrderManagerHandle { sender: Sender, @@ -344,7 +344,36 @@ impl OrderManagerHandle { self.sender .send(ActorMessage { - message: Message::ClosePosition { position_id }, + message: ActionMessage::ClosePosition { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn close_position_orders( + &mut self, + position_id: u64, + ) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::ClosePositionOrders { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn submit_order(&mut self, order_form: OrderForm) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::SubmitOrder { order: order_form }, respond_to: send, }) .await?; @@ -381,9 +410,14 @@ impl OrderManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { .. } => self.update().await?, - Message::ClosePosition { position_id } => self.close_position(position_id).await?, - _ => (None, None), + ActionMessage::Update { .. } => self.update().await?, + ActionMessage::ClosePosition { position_id } => { + self.close_position(position_id).await? + } + ActionMessage::ClosePositionOrders { position_id } => { + self.close_position_orders(position_id).await? + } + ActionMessage::SubmitOrder { order } => self.submit_order(&order).await?, }; Ok(msg @@ -392,6 +426,53 @@ impl OrderManager { .map_err(|_| BoxError::from("Could not send message."))?) } + pub async fn close_position_orders(&self, position_id: u64) -> Result { + info!("Closing outstanding orders for position #{}", position_id); + + if let Some(position_orders) = self.tracked_positions.get(&position_id) { + // retrieving open orders + let open_orders = self.client.active_orders(&self.pair).await?; + let position_orders: Vec<_> = position_orders + .iter() + .filter_map(|&x| open_orders.iter().find(|y| y.id == x)) + .collect(); + + for order in position_orders { + match self.client.cancel_order(order).await { + Ok(_) => info!("Order #{} closed successfully.", order.id), + Err(e) => error!("Could not close order #{}: {}", order.id, e), + } + } + } + + // TODO: return valid messages and events! + Ok((None, None)) + } + + pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result { + info!("Submiting {}", order_form.kind()); + + let active_order = self.client.submit_order(order_form).await?; + + debug!("Adding order to tracked orders."); + if let Some(metadata) = order_form.metadata() { + if let Some(position_id) = metadata.position_id() { + match self.tracked_positions.get_mut(&position_id) { + None => { + self.tracked_positions + .insert(position_id, vec![active_order.id]); + } + Some(position_orders) => { + position_orders.push(active_order.id); + } + } + } + }; + + // TODO: return valid messages and events!111!!!1! + Ok((None, None)) + } + pub async fn close_position(&mut self, position_id: u64) -> Result { info!("Closing position #{}", position_id); @@ -450,8 +531,8 @@ impl OrderManager { Ok((None, None)) } - pub async fn update(&self) -> Result { - trace!("\t[OrderManager] Updating {}", self.pair); + pub async fn update(&mut self) -> Result { + debug!("\t[OrderManager] Updating {}", self.pair); let (res_open_orders, res_order_book) = tokio::join!( self.client.active_orders(&self.pair), @@ -460,8 +541,49 @@ impl OrderManager { let (open_orders, order_book) = (res_open_orders?, res_order_book?); + // retrieving open positions to check whether the positions have open orders. + // we need to update our internal mapping in that case. + if !open_orders.is_empty() { + let open_positions = self.client.active_positions(&self.pair).await?; + + if let Some(positions) = open_positions { + // currently, we are only trying to match orders with an amount equal to + // a position amount. + for position in positions { + let matching_order = open_orders + .iter() + .find(|x| x.details.amount().abs() == position.amount().abs()); + + // if an order is found, we insert the order to our internal mapping, if not already present + if let Some(matching_order) = matching_order { + match self.tracked_positions.get_mut(&position.id()) { + Some(position_orders) => { + if !position_orders.contains(&matching_order.id) { + trace!( + "Mapped order #{} to position #{}", + position.id(), + matching_order.id + ); + position_orders.push(matching_order.id); + } + } + None => { + trace!( + "Mapped order #{} to position #{}", + position.id(), + matching_order.id + ); + self.tracked_positions + .insert(position.id(), vec![matching_order.id]); + } + } + } + } + } + } + for active_order in open_orders { - debug!( + trace!( "Found open order, calling \"{}\" strategy.", self.strategy.name() ); @@ -471,7 +593,7 @@ impl OrderManager { if let Some(messages) = strat_messages { for m in messages { match m { - Message::SubmitOrder { order: order_form } => { + ActionMessage::SubmitOrder { order: order_form } => { info!("Closing open order..."); info!("\tCancelling open order #{}", &active_order.id); self.client.cancel_order(&active_order).await?; @@ -539,7 +661,7 @@ impl PairManager { position_manager: PositionManagerHandle::new( pair, client, - Box::new(HiddenTrailingStop::new()), + Box::new(HiddenTrailingStop::default()), ), } } @@ -565,8 +687,19 @@ impl PairManager { // TODO: to move into Handler? if let Some(messages) = messages { for m in messages { - if let Message::ClosePosition { position_id } = m { - self.order_manager.close_position(position_id).await?; + match m { + ActionMessage::Update { .. } => {} + ActionMessage::ClosePosition { position_id } => { + self.order_manager.close_position(position_id).await?; + } + ActionMessage::SubmitOrder { order } => { + self.order_manager.submit_order(order).await?; + } + ActionMessage::ClosePositionOrders { position_id } => { + self.order_manager + .close_position_orders(position_id) + .await?; + } } } } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 8873578..01fa2f6 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -265,6 +265,7 @@ pub struct OrderForm { platform: TradingPlatform, amount: f64, leverage: Option, + metadata: Option, } impl OrderForm { @@ -280,6 +281,7 @@ impl OrderForm { platform, amount, leverage: None, + metadata: None, } } @@ -288,6 +290,11 @@ impl OrderForm { self } + pub fn with_metadata(mut self, metadata: OrderMetadata) -> Self { + self.metadata = Some(metadata); + self + } + pub fn pair(&self) -> &SymbolPair { &self.pair } @@ -319,6 +326,27 @@ impl OrderForm { pub fn leverage(&self) -> Option { self.leverage } + + pub fn metadata(&self) -> &Option { + &self.metadata + } +} + +#[derive(Debug, Clone)] +pub struct OrderMetadata { + position_id: Option, +} + +impl OrderMetadata { + pub fn with_position_id(position_id: u64) -> Self { + OrderMetadata { + position_id: Some(position_id), + } + } + + pub fn position_id(&self) -> Option { + self.position_id + } } /*************** diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index e4fe8da..23d4ecd 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -1,13 +1,16 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; + use dyn_clone::DynClone; -use log::info; +use log::{info}; use crate::connectors::Connector; -use crate::events::{Event, EventKind, EventMetadata, Message}; +use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::managers::OptionUpdate; -use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; +use crate::models::{ + ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState, +}; use crate::BoxError; /*************** @@ -21,13 +24,13 @@ pub trait PositionStrategy: DynClone + Send + Sync { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); fn post_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); } impl Debug for dyn PositionStrategy { @@ -69,66 +72,47 @@ impl Debug for dyn OrderStrategy { #[derive(Clone, Debug)] pub struct HiddenTrailingStop { stop_percentages: 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 HiddenTrailingStop { - // in percentage - const CAPITAL_MAX_LOSS: f64 = 15.0; - const CAPITAL_MIN_PROFIT: f64 = 9.0; - const CAPITAL_GOOD_PROFIT: f64 = HiddenTrailingStop::CAPITAL_MIN_PROFIT * 2.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 = (HiddenTrailingStop::CAPITAL_MIN_PROFIT - / HiddenTrailingStop::LEVERAGE) - + HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA; - const GOOD_PROFIT_PERC: f64 = (HiddenTrailingStop::CAPITAL_GOOD_PROFIT - / HiddenTrailingStop::LEVERAGE) - + HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA; - const MAX_LOSS_PERC: f64 = - -(HiddenTrailingStop::CAPITAL_MAX_LOSS / HiddenTrailingStop::LEVERAGE); - - pub fn new() -> Self { - HiddenTrailingStop { - 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(HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA) - } - PositionProfitState::Profit => Some(HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA), + 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; - match profit_state { - PositionProfitState::MinimumProfit | PositionProfitState::Profit => { - match self.stop_percentages.get(&position.id()) { - None => { + 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); } - Some(existing_threshold) => { - if existing_threshold < ¤t_stop_percentage { - self.stop_percentages - .insert(position.id(), current_stop_percentage); - } - } } } - _ => {} } } + info!( "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", position.profit_state().unwrap(), @@ -141,6 +125,40 @@ impl HiddenTrailingStop { } } +impl Default for HiddenTrailingStop { + fn default() -> Self { + let leverage = 5.0; + + // in percentage + let capital_max_loss = 15.0; + let capital_min_profit = 9.0; + 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; + + HiddenTrailingStop { + stop_percentages: 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 HiddenTrailingStop { fn name(&self) -> String { "Hidden Trailing Stop".into() @@ -152,19 +170,17 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>) { + ) -> (Position, Option>, Option>) { let pl_perc = position.pl_perc(); let state = { - if pl_perc > HiddenTrailingStop::GOOD_PROFIT_PERC { + if pl_perc > self.good_profit_percentage { PositionProfitState::Profit - } else if HiddenTrailingStop::MIN_PROFIT_PERC <= pl_perc - && pl_perc < HiddenTrailingStop::GOOD_PROFIT_PERC - { + } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { PositionProfitState::MinimumProfit - } else if (0.0..HiddenTrailingStop::MIN_PROFIT_PERC).contains(&pl_perc) { + } else if (0.0..self.min_profit_percentage).contains(&pl_perc) { PositionProfitState::BreakEven - } else if (HiddenTrailingStop::MAX_LOSS_PERC..0.0).contains(&pl_perc) { + } else if (self.max_loss_percentage..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical @@ -230,8 +246,8 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, _: u64, _: &HashMap, - ) -> (Position, Option>, Option>) { - let close_message = Message::ClosePosition { + ) -> (Position, Option>, Option>) { + let close_message = ActionMessage::ClosePosition { position_id: position.id(), }; @@ -255,6 +271,263 @@ impl PositionStrategy for HiddenTrailingStop { } } +// #[derive(Clone, Debug)] +// pub struct TrailingStop { +// stop_percentages: 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 update_stop_percentage(&mut self, position: &Position) -> Option { +// let mut order_form = None; +// +// 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; +// let price_percentage_delta = { +// if position.is_short() { +// // 1.0 is the base price +// 1.0 - current_stop_percentage / 100.0 +// } else { +// 1.0 + current_stop_percentage / 100.0 +// } +// }; +// +// println!("Delta: {}", price_percentage_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); +// +// trace!("Setting trailing stop, asking order manager to cancel previous orders."); +// order_form = Some( +// OrderForm::new( +// position.pair().clone(), +// OrderKind::Limit { +// price: position.base_price() * price_percentage_delta, +// }, +// position.platform(), +// position.amount().neg(), +// ) +// .with_metadata(OrderMetadata::with_position_id(position.id())), +// ); +// } +// Some(existing_threshold) => { +// // follow and update trailing stop +// if existing_threshold < ¤t_stop_percentage { +// self.stop_percentages +// .insert(position.id(), current_stop_percentage); +// +// trace!("Updating threshold, asking order manager to cancel previous orders."); +// order_form = Some( +// OrderForm::new( +// position.pair().clone(), +// OrderKind::Limit { +// price: position.base_price() * price_percentage_delta, +// }, +// position.platform(), +// position.amount().neg(), +// ) +// .with_metadata(OrderMetadata::with_position_id(position.id())), +// ); +// } +// } +// } +// } +// } +// +// info!( +// "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", +// position.profit_state().unwrap(), +// position.pl(), +// position.pair().quote(), +// position.pl_perc(), +// self.stop_percentages.get(&position.id()).unwrap_or(&0.0) +// ); +// } +// +// order_form +// } +// } +// +// impl Default for TrailingStop { +// fn default() -> Self { +// let leverage = 5.0; +// +// // in percentage +// let capital_max_loss = 15.0; +// let capital_min_profit = 1.0; +// let capital_good_profit = 6.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.2; +// 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(), +// 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 { +// "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 > 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)); +// +// 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 +// }; +// +// (new_position, Some(events), None) +// } +// +// fn post_tick( +// &mut self, +// position: Position, +// _: u64, +// _: &HashMap, +// ) -> (Position, Option>, Option>) { +// let close_message = ActionMessage::ClosePosition { +// position_id: position.id(), +// }; +// +// // if critical, early return with close position +// if let Some(PositionProfitState::Critical) = position.profit_state() { +// 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])); +// } +// } +// +// // updated or new trailing stop. should cancel orders and submit new one +// if let Some(order_form) = self.update_stop_percentage(&position) { +// let mut messages = vec![]; +// +// messages.push(ActionMessage::ClosePositionOrders { +// position_id: position.id(), +// }); +// messages.push(ActionMessage::SubmitOrder { order: order_form }); +// +// return (position, None, Some(messages)); +// } +// +// (position, None, None) +// } +// } + +/* + * ORDER STRATEGIES + */ + #[derive(Clone, Debug)] pub struct MarketEnforce { // threshold (%) for which we trigger a market order @@ -264,7 +537,9 @@ pub struct MarketEnforce { impl Default for MarketEnforce { fn default() -> Self { - Self { threshold: 0.15 } + Self { + threshold: 1.0 / 15.0, + } } } @@ -298,7 +573,7 @@ impl OrderStrategy for MarketEnforce { let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; if delta > self.threshold { - messages.push(Message::SubmitOrder { + messages.push(ActionMessage::SubmitOrder { order: OrderForm::new( order.symbol.clone(), OrderKind::Market,