407 lines
13 KiB
Rust
407 lines
13 KiB
Rust
use std::collections::HashMap;
|
|
use std::fmt::{Debug, Formatter};
|
|
use std::ops::Neg;
|
|
|
|
use dyn_clone::DynClone;
|
|
use log::info;
|
|
|
|
use crate::BoxError;
|
|
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
|
|
use crate::managers::OptionUpdate;
|
|
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees};
|
|
|
|
/***************
|
|
* 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>,
|
|
fees: &Vec<TradingFees>,
|
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
|
fn post_tick(
|
|
&mut self,
|
|
position: Position,
|
|
current_tick: u64,
|
|
positions_history: &HashMap<u64, Position>,
|
|
fees: &Vec<TradingFees>,
|
|
) -> (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 {
|
|
// position id: stop_percentage
|
|
stop_percentages: HashMap<u64, f64>,
|
|
// position_id: bool
|
|
stop_loss_flags: HashMap<u64, bool>,
|
|
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 print_status(&self, position: &Position) {
|
|
match self.stop_percentages.get(&position.id()) {
|
|
None => {
|
|
info!(
|
|
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%)",
|
|
position.profit_state().unwrap(),
|
|
position.pl(),
|
|
position.pair().quote(),
|
|
position.pl_perc()
|
|
);
|
|
}
|
|
Some(stop_percentage) => {
|
|
info!(
|
|
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
|
|
position.profit_state().unwrap(),
|
|
position.pl(),
|
|
position.pair().quote(),
|
|
position.pl_perc(),
|
|
stop_percentage
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 < ¤t_stop_percentage {
|
|
self.stop_percentages
|
|
.insert(position.id(), current_stop_percentage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for HiddenTrailingStop {
|
|
fn default() -> Self {
|
|
let leverage = 15.0;
|
|
|
|
// in percentage
|
|
let capital_max_loss = 15.0;
|
|
let capital_min_profit = 8.5;
|
|
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(),
|
|
stop_loss_flags: 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>,
|
|
_: &Vec<TradingFees>,
|
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
|
let pl_perc = position.pl_perc();
|
|
|
|
// setting the state of the position based on its profit/loss percentage
|
|
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));
|
|
|
|
// checking if there was a state change between the current position
|
|
// and its last 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 event = match state {
|
|
PositionProfitState::Critical => {
|
|
Event::new(
|
|
EventKind::ReachedMaxLoss,
|
|
current_tick,
|
|
)
|
|
}
|
|
PositionProfitState::Loss => {
|
|
Event::new(
|
|
EventKind::ReachedLoss,
|
|
current_tick,
|
|
)
|
|
}
|
|
PositionProfitState::BreakEven => {
|
|
Event::new(
|
|
EventKind::ReachedBreakEven,
|
|
current_tick,
|
|
)
|
|
}
|
|
PositionProfitState::MinimumProfit => {
|
|
Event::new(
|
|
EventKind::ReachedMinProfit,
|
|
current_tick,
|
|
)
|
|
}
|
|
PositionProfitState::Profit => {
|
|
Event::new(
|
|
EventKind::ReachedGoodProfit,
|
|
current_tick,
|
|
)
|
|
}
|
|
}.with_metadata(Some(event_metadata));
|
|
|
|
(new_position, Some(vec![event]), None)
|
|
}
|
|
|
|
fn post_tick(
|
|
&mut self,
|
|
position: Position,
|
|
_: u64,
|
|
_: &HashMap<u64, Position>,
|
|
fees: &Vec<TradingFees>,
|
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
|
self.print_status(&position);
|
|
|
|
let taker_fee = fees
|
|
.iter()
|
|
.filter_map(|x| match x {
|
|
TradingFees::Taker {
|
|
platform,
|
|
percentage,
|
|
} if platform == &position.platform() => Some(percentage),
|
|
_ => None,
|
|
})
|
|
.next().map_or_else(|| 0.0, |&x| x);
|
|
|
|
let stop_loss_price = {
|
|
if position.is_short() {
|
|
position.base_price() * (1.0 - (self.max_loss_percentage - taker_fee) / 100.0)
|
|
} else {
|
|
position.base_price() * (1.0 + (self.max_loss_percentage - taker_fee) / 100.0)
|
|
}
|
|
};
|
|
let close_position_orders_msg = ActionMessage::ClosePositionOrders {
|
|
position_id: position.id(),
|
|
};
|
|
let close_position_msg = ActionMessage::ClosePosition {
|
|
position_id: position.id(),
|
|
};
|
|
let set_stop_loss_msg = ActionMessage::SubmitOrder {
|
|
order: OrderForm::new(position.pair().clone(),
|
|
OrderKind::Stop { price: stop_loss_price },
|
|
position.platform(),
|
|
position.amount().neg())
|
|
.with_leverage(Some(self.leverage))
|
|
.with_metadata(Some(OrderMetadata::new().with_position_id(Some(position.id()))))
|
|
};
|
|
let stop_loss_set = *self.stop_loss_flags.entry(position.id()).or_insert(false);
|
|
|
|
// if in loss, ask the order manager to set the stop limit order,
|
|
// if not already set
|
|
if let Some(PositionProfitState::Critical) | Some(PositionProfitState::Loss) = position.profit_state() {
|
|
if !stop_loss_set {
|
|
info!("In loss. Opening trailing stop order.");
|
|
|
|
self.stop_loss_flags.insert(position.id(), true);
|
|
return (position, None, Some(vec![set_stop_loss_msg]));
|
|
}
|
|
return (position, None, None);
|
|
}
|
|
|
|
let mut messages = vec![];
|
|
|
|
// if a stop loss order was previously set,
|
|
// ask the order manager to remove the order first
|
|
if stop_loss_set {
|
|
info!("Removing stop loss order.");
|
|
messages.push(close_position_orders_msg);
|
|
self.stop_loss_flags.insert(position.id(), false);
|
|
}
|
|
|
|
// 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.");
|
|
messages.push(close_position_msg);
|
|
return (position, None, Some(messages));
|
|
}
|
|
}
|
|
|
|
self.update_stop_percentage(&position);
|
|
|
|
(position, None, Some(messages))
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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.2 / 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)))
|
|
}
|
|
}
|