253 lines
7.4 KiB
Rust
253 lines
7.4 KiB
Rust
use std::collections::HashMap;
|
|
use std::fmt::{Debug, Formatter};
|
|
|
|
use dyn_clone::DynClone;
|
|
use log::{debug, info};
|
|
use tokio::sync::oneshot;
|
|
|
|
use crate::events::{Event, EventKind, EventMetadata, Message};
|
|
use crate::managers::{OrderManager, PositionManager, TrackedPositionsMap};
|
|
use crate::models::{
|
|
ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState,
|
|
};
|
|
use crate::BoxError;
|
|
|
|
/***************
|
|
* DEFINITIONS
|
|
***************/
|
|
|
|
pub trait PositionStrategy: DynClone + Send {
|
|
fn name(&self) -> String;
|
|
fn on_new_tick(
|
|
&self,
|
|
position: Position,
|
|
manager: &PositionManager,
|
|
) -> (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 {
|
|
/// 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_update(&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_close(
|
|
&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.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 = -0.01;
|
|
|
|
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 name(&self) -> String {
|
|
"Trailing stop".into()
|
|
}
|
|
|
|
fn on_new_tick(
|
|
&self,
|
|
position: Position,
|
|
manager: &PositionManager,
|
|
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>) {
|
|
let mut messages = vec![];
|
|
let events = vec![];
|
|
|
|
let pl_perc = TrailingStop::net_pl_percentage(position.pl_perc(), TrailingStop::TAKER_FEE);
|
|
|
|
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 {
|
|
debug!("Inserting close position message...");
|
|
messages.push(Message::ClosePosition {
|
|
position: position.clone(),
|
|
order_kind: OrderKind::Limit,
|
|
});
|
|
PositionProfitState::Critical
|
|
}
|
|
};
|
|
|
|
let opt_pre_pw = manager.position_previous_tick(position.id(), None);
|
|
let event_metadata = EventMetadata::new(Some(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.is_empty()).then_some(events),
|
|
(!messages.is_empty()).then_some(messages),
|
|
);
|
|
}
|
|
}
|
|
None => {
|
|
return (
|
|
new_position,
|
|
(!events.is_empty()).then_some(events),
|
|
(!messages.is_empty()).then_some(messages),
|
|
)
|
|
}
|
|
};
|
|
|
|
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.is_empty()).then_some(events),
|
|
(!messages.is_empty()).then_some(messages),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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_update(&self) {
|
|
unimplemented!()
|
|
}
|
|
|
|
fn on_position_close(
|
|
&self,
|
|
order: &ActiveOrder,
|
|
active_position: &Position,
|
|
order_book: &OrderBook,
|
|
) -> Result<(Option<Vec<Event>>, Option<Vec<Message>>), BoxError> {
|
|
let mut messages = vec![];
|
|
|
|
// long
|
|
let offer_comparison = {
|
|
if order.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 delta = (1.0 - (offer_comparison / order.price)) * 100.0;
|
|
|
|
debug!(
|
|
"Offer comp: {} | Our offer: {} | Current delta: {}",
|
|
offer_comparison, order.price, delta
|
|
);
|
|
|
|
if delta > self.threshold {
|
|
messages.push(Message::ClosePosition {
|
|
position: active_position.clone(),
|
|
order_kind: OrderKind::Market,
|
|
})
|
|
}
|
|
|
|
Ok((None, (!messages.is_empty()).then_some(messages)))
|
|
}
|
|
}
|