core/rustybot/src/strategy.rs
2021-01-24 20:50:19 +00:00

320 lines
9.9 KiB
Rust

use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::ops::Neg;
use dyn_clone::DynClone;
use log::{debug, info};
use tokio::sync::{oneshot, RwLock};
use crate::events::{Event, EventKind, EventMetadata, Message};
use crate::managers::{OrderManager, PositionManager, TrackedPositionsMap};
use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState,
PositionState, TradingPlatform,
};
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<Message>>);
fn post_tick(
&mut self,
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>);
}
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);
/// 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<(Option<Vec<Event>>, Option<Vec<Message>>), 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 TrailingStop {
stop_percentages: HashMap<u64, f64>,
}
impl TrailingStop {
const BREAK_EVEN_PERC: f64 = 0.01;
const MIN_PROFIT_PERC: f64 = TrailingStop::BREAK_EVEN_PERC + 0.03;
const GOOD_PROFIT_PERC: f64 = TrailingStop::MIN_PROFIT_PERC * 2.5;
const MAX_LOSS_PERC: f64 = -0.5;
const TAKER_FEE: f64 = 0.2;
pub fn new() -> Self {
TrailingStop {
stop_percentages: HashMap::new(),
}
}
fn update_stop_percentage(&mut self, position: &Position) {
println!(
"State: {:?} | PL%: {:0.2}",
position.profit_state(),
position.pl_perc()
);
if let Some(profit_state) = position.profit_state() {
let profit_state_delta = match profit_state {
PositionProfitState::MinimumProfit => Some(Self::MIN_PROFIT_PERC),
PositionProfitState::Profit => Some(Self::GOOD_PROFIT_PERC),
_ => None,
};
if let Some(profit_state_delta) = profit_state_delta {
println!(
"PL%: {} | Delta: {}",
position.pl_perc(),
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 => {
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);
}
}
}
}
_ => {}
}
}
}
}
}
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<Message>>) {
let pl_perc = position.pl_perc();
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 {
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.clone().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
};
return (new_position, Some(events), None);
}
fn post_tick(
&mut self,
position: Position,
_: u64,
_: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>) {
let close_message = Message::ClosePosition {
position_id: position.id(),
};
// if critical, early return with close position
if let Some(profit_state) = position.profit_state() {
match profit_state {
PositionProfitState::Critical => {
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 existing_stop_percentage <= &position.pl_perc() {
return (position, None, Some(vec![close_message]));
}
}
self.update_stop_percentage(&position);
println!("Stop percentages: {:?}", self.stop_percentages);
(position, None, None)
}
}
#[derive(Clone, Debug)]
pub struct FastOrderStrategy {
// threshold (%) for which we trigger a market order
// to close an open position
threshold: f64,
}
impl Default for FastOrderStrategy {
fn default() -> Self {
Self { threshold: 0.2 }
}
}
impl FastOrderStrategy {
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
}
impl OrderStrategy for FastOrderStrategy {
fn name(&self) -> String {
"Fast order strategy".into()
}
fn on_open_order(&self) {
unimplemented!()
}
fn on_position_order(
&self,
order: &ActiveOrder,
_: &Position,
order_book: &OrderBook,
) -> Result<(Option<Vec<Event>>, Option<Vec<Message>>), BoxError> {
let mut messages = vec![];
// long
let offer_comparison = {
if order.current_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
.current_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(Message::SubmitOrder {
order: OrderForm::new(
order.symbol.clone(),
OrderKind::Market {
amount: order.current_form.amount(),
},
order.current_form.platform().clone(),
),
})
}
Ok((None, (!messages.is_empty()).then_some(messages)))
}
}