core/rustybot/src/strategy.rs
2021-02-17 16:32:08 +00:00

588 lines
21 KiB
Rust

use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use dyn_clone::DynClone;
use log::info;
use crate::connectors::Connector;
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
use crate::managers::OptionUpdate;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
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<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
fn post_tick(
&mut self,
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
}
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<OptionUpdate, BoxError>;
// /// 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<OptionUpdate, 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 HiddenTrailingStop {
stop_percentages: HashMap<u64, f64>,
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 {
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(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;
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 < &current_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(),
position.pl(),
position.pair().quote(),
position.pl_perc(),
self.stop_percentages.get(&position.id()).unwrap_or(&0.0)
);
}
}
}
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()
}
/// Sets the profit state of an open position
fn on_tick(
&mut self,
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
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<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
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]));
}
}
self.update_stop_percentage(&position);
(position, None, None)
}
}
// #[derive(Clone, Debug)]
// pub struct TrailingStop {
// stop_percentages: HashMap<u64, f64>,
// 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<OrderForm> {
// 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 < &current_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<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// 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<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// 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
// to close an open position
threshold: f64,
}
impl Default for MarketEnforce {
fn default() -> Self {
Self {
threshold: 1.0 / 15.0,
}
}
}
impl OrderStrategy for MarketEnforce {
fn name(&self) -> String {
"Market Enforce".into()
}
fn on_open_order(
&self,
order: &ActiveOrder,
order_book: &OrderBook,
) -> Result<OptionUpdate, BoxError> {
let mut messages = vec![];
// long
let offer_comparison = {
if order.order_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
.order_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(ActionMessage::SubmitOrder {
order: OrderForm::new(
order.pair().clone(),
OrderKind::Market,
*order.order_form().platform(),
order.order_form().amount(),
)
.with_leverage(order.order_form().leverage())
.with_metadata(order.order_form().metadata().clone()),
})
}
Ok((None, (!messages.is_empty()).then_some(messages)))
}
}