From dfd676612e207b67460e9134da09183454aae6c7 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 16 Jan 2021 11:43:16 +0000 Subject: [PATCH] stuff... --- rustybot/Cargo.lock | 1 + rustybot/Cargo.toml | 3 +- rustybot/src/connectors.rs | 100 +++++++++++++++++++++++----------- rustybot/src/events.rs | 13 ++--- rustybot/src/managers.rs | 106 ++++++++++++++++++++++++------------- rustybot/src/models.rs | 81 +++++++++++++++++++++++----- rustybot/src/strategy.rs | 69 +++++++++++++++++++----- 7 files changed, 271 insertions(+), 102 deletions(-) diff --git a/rustybot/Cargo.lock b/rustybot/Cargo.lock index b3e1cbe..92cc2d1 100644 --- a/rustybot/Cargo.lock +++ b/rustybot/Cargo.lock @@ -1128,6 +1128,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bitfinex", + "byteorder", "chrono", "dyn-clone", "fern", diff --git a/rustybot/Cargo.toml b/rustybot/Cargo.toml index 490fa04..eecc05d 100644 --- a/rustybot/Cargo.toml +++ b/rustybot/Cargo.toml @@ -16,4 +16,5 @@ regex = "1" dyn-clone = "1" log = "0.4" fern = {version = "0.6", features = ["colored"]} -chrono = "0.4" \ No newline at end of file +chrono = "0.4" +byteorder = "1" \ No newline at end of file diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index c3a675a..c80a17c 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -4,11 +4,11 @@ use std::sync::Arc; use async_trait::async_trait; use bitfinex::api::Bitfinex; -use bitfinex::orders::{OrderForm, OrderMeta}; +use bitfinex::orders::{ActiveOrder, OrderMeta}; use bitfinex::ticker::TradingPairTicker; use crate::currency::SymbolPair; -use crate::models::{Order, OrderKind, Position, PositionState, PriceTicker}; +use crate::models::{ExecutedOrder, OrderForm, OrderKind, Position, PositionState, PriceTicker}; use crate::BoxError; #[derive(Eq, PartialEq, Hash, Clone, Debug)] @@ -50,18 +50,12 @@ impl Client { self.inner.current_prices(pair).await } - pub async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { + pub async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { self.inner.active_orders(pair).await } - pub async fn submit_order( - &self, - pair: &SymbolPair, - amount: f64, - price: f64, - kind: &OrderKind, - ) -> Result<(), BoxError> { - self.inner.submit_order(pair, amount, price, kind).await + pub async fn submit_order(&self, order: OrderForm) -> Result { + self.inner.submit_order(order).await } } @@ -70,14 +64,8 @@ pub trait Connector: Send + Sync { fn name(&self) -> String; async fn active_positions(&self, pair: &SymbolPair) -> Result>, BoxError>; async fn current_prices(&self, pair: &SymbolPair) -> Result; - async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError>; - async fn submit_order( - &self, - pair: &SymbolPair, - amount: f64, - price: f64, - kind: &OrderKind, - ) -> Result<(), BoxError>; + async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError>; + async fn submit_order(&self, order: OrderForm) -> Result; } impl Debug for dyn Connector { @@ -148,24 +136,28 @@ impl Connector for BitfinexConnector { Ok(ticker) } - async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { + async fn active_orders(&self, pair: &SymbolPair) -> Result, BoxError> { unimplemented!() } - async fn submit_order( - &self, - pair: &SymbolPair, - amount: f64, - price: f64, - kind: &OrderKind, - ) -> Result<(), BoxError> { + async fn submit_order(&self, order: OrderForm) -> Result { let order_form = match &self.affiliate_code { - Some(affiliate_code) => OrderForm::new(pair.trading_repr(), price, amount, kind.into()) - .with_meta(OrderMeta::new(affiliate_code.clone())), - None => OrderForm::new(pair.trading_repr(), price, amount, kind.into()), + Some(affiliate_code) => bitfinex::orders::OrderForm::new( + order.pair().trading_repr(), + *order.price(), + *order.amount(), + order.kind().into(), + ) + .with_meta(OrderMeta::new(affiliate_code.clone())), + None => bitfinex::orders::OrderForm::new( + order.pair().trading_repr(), + *order.price(), + *order.amount(), + order.kind().into(), + ), }; - Ok(self.bfx.orders.submit_order(&order_form).await?) + Ok(self.bfx.orders.submit_order(&order_form).await?.into()) } } @@ -216,6 +208,26 @@ impl From<&OrderKind> for bitfinex::orders::OrderKind { } } +impl From for bitfinex::orders::OrderKind { + fn from(o: OrderKind) -> Self { + match o { + OrderKind::Limit => Self::Limit, + OrderKind::ExchangeLimit => Self::ExchangeLimit, + OrderKind::Market => Self::Market, + OrderKind::ExchangeMarket => Self::ExchangeMarket, + OrderKind::Stop => Self::Stop, + OrderKind::ExchangeStop => Self::ExchangeStop, + OrderKind::StopLimit => Self::StopLimit, + OrderKind::ExchangeStopLimit => Self::ExchangeStopLimit, + OrderKind::TrailingStop => Self::TrailingStop, + OrderKind::Fok => Self::Fok, + OrderKind::ExchangeFok => Self::ExchangeFok, + OrderKind::Ioc => Self::Ioc, + OrderKind::ExchangeIoc => Self::ExchangeIoc, + } + } +} + impl From for PriceTicker { fn from(t: TradingPairTicker) -> Self { Self { @@ -232,3 +244,29 @@ impl From for PriceTicker { } } } + +impl From for ExecutedOrder { + fn from(o: ActiveOrder) -> Self { + Self { + id: o.id, + group_id: o.group_id, + client_id: o.client_id, + symbol: o.symbol, + creation_timestamp: o.creation_timestamp, + update_timestamp: o.update_timestamp, + amount: o.amount, + amount_original: o.amount_original, + order_type: o.order_type, + previous_order_type: o.previous_order_type, + flags: o.flags, + order_status: o.order_status, + price: o.price, + price_avg: o.price_avg, + price_trailing: o.price_trailing, + price_aux_limit: o.price_aux_limit, + notify: o.notify, + hidden: o.hidden, + placed_id: o.placed_id, + } + } +} diff --git a/rustybot/src/events.rs b/rustybot/src/events.rs index 6229c3f..4e6868e 100644 --- a/rustybot/src/events.rs +++ b/rustybot/src/events.rs @@ -3,12 +3,12 @@ use std::future::Future; use tokio::task::JoinHandle; -use crate::managers::PriceManager; +use crate::managers::{OrderManager, PositionManager, PriceManager}; use crate::models::{Position, PositionProfitState}; -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum SignalKind { - ClosePosition { position_id: u64 }, + ClosePosition(Position), OpenPosition, } @@ -74,18 +74,19 @@ impl Event { } } -pub struct EventDispatcher { +pub struct Dispatcher { event_handlers: HashMap JoinHandle<()>>>>, profit_state_handlers: HashMap JoinHandle<()>>>>, signal_handlers: HashMap JoinHandle<()>>>>, + on_any_event_handlers: Vec JoinHandle<()>>>, on_any_profit_state_handlers: Vec JoinHandle<()>>>, } -impl EventDispatcher { +impl Dispatcher { pub fn new() -> Self { - EventDispatcher { + Dispatcher { event_handlers: HashMap::new(), profit_state_handlers: HashMap::new(), signal_handlers: HashMap::new(), diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index 2614211..5c636dd 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -1,10 +1,14 @@ use std::collections::HashMap; +use std::ops::Neg; + +use bitfinex::ticker::TradingPairTicker; +use log::error; use crate::connectors::{Client, ExchangeKind}; use crate::currency::SymbolPair; -use crate::events::{Event, SignalKind}; -use crate::models::{Order, OrderForm, Position, PriceTicker}; -use crate::strategy::PositionStrategy; +use crate::events::{Dispatcher, Event, SignalKind}; +use crate::models::{ExecutedOrder, OrderForm, OrderKind, Position, PriceTicker}; +use crate::strategy::{FastOrderStrategy, OrderStrategy, PositionStrategy}; use crate::BoxError; pub struct EventManager { @@ -43,34 +47,6 @@ impl PriceManager { )) } - // pub fn add_position(&mut self, position: Position) { - // let (new_position, events, signals) = { - // match &self.strategy { - // Some(strategy) => strategy.on_new_tick(&position, &self), - // None => (position, vec![], vec![]), - // } - // }; - // - // self.positions - // .entry(self.current_tick) - // .or_default() - // .push(new_position.clone()); - // - // // calling position state callbacks - // self.dispatcher - // .call_position_state_handlers(&new_position, &self); - // - // // adding events and calling callbacks - // for e in events { - // self.add_event(e); - // } - // - // // adding signals to current tick vector - // for s in signals { - // self.add_signal(s); - // } - // } - // fn add_event(&mut self, event: Event) { // self.events.push(event); // @@ -190,7 +166,7 @@ impl PositionManager { match &self.strategy { Some(strategy) => { let (pos, strategy_events, _) = - strategy.on_new_tick(&position, &self); + strategy.on_new_tick(position, &self); events.extend(strategy_events); @@ -225,29 +201,81 @@ impl PositionManager { self.positions_history .get(&tick) - .filter(|x| x.position_id() == id) + .filter(|x| x.id() == id) .and_then(|x| Some(x)) } } pub struct OrderManager { + tracked_positions: HashMap, pair: SymbolPair, - open_orders: Vec, + open_orders: Vec, client: Client, + strategy: Box, } impl OrderManager { - pub fn new(pair: SymbolPair, client: Client) -> Self { + const UNDERCUT_PERC: f64 = 0.005; + + pub fn new(pair: SymbolPair, client: Client, strategy: Box) -> Self { OrderManager { pair, open_orders: Vec::new(), client, + strategy, + tracked_positions: HashMap::new(), } } + pub async fn close_position(&mut self, position: &Position) -> Result<(), BoxError> { + // checking if the position has an open order. + // If so, the strategy method is called, otherwise we open + // an undercut limit order at the best current price. + match self.tracked_positions.get(&position.id()) { + Some(open_order) => self.strategy.on_position_close(open_order, &self), + None => { + let current_prices = self.client.current_prices(&self.pair).await?; + let closing_price = self.best_closing_price(&position, ¤t_prices)?; + + // submitting order + let order_form = OrderForm::new( + &self.pair, + closing_price, + position.amount().neg(), + OrderKind::Limit, + ); + + match self.client.submit_order(order_form).await { + Err(e) => error!("Could not submit order: {}", e), + Ok(o) => { + self.tracked_positions.insert(position.id(), o); + } + }; + } + } + + Ok(()) + } + pub fn update(&self) -> Option> { unimplemented!() } + + pub fn best_closing_price( + &self, + position: &Position, + price_ticker: &TradingPairTicker, + ) -> Result { + let price = { + if position.is_short() { + price_ticker.ask + } else { + price_ticker.bid + } + }; + + Ok(price * (1.0 - OrderManager::UNDERCUT_PERC)) + } } pub struct ExchangeManager { @@ -255,6 +283,7 @@ pub struct ExchangeManager { price_managers: Vec, order_managers: Vec, position_managers: Vec, + dispatcher: Dispatcher, client: Client, } @@ -268,7 +297,11 @@ impl ExchangeManager { for p in pairs { position_managers.push(PositionManager::new(p.clone(), client.clone())); - order_managers.push(OrderManager::new(p.clone(), client.clone())); + order_managers.push(OrderManager::new( + p.clone(), + client.clone(), + Box::new(FastOrderStrategy {}), + )); price_managers.push(PriceManager::new(p.clone(), client.clone())); } @@ -278,6 +311,7 @@ impl ExchangeManager { order_managers, price_managers, client, + dispatcher: Dispatcher::new(), } } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 31aa050..e2b53cb 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -1,7 +1,10 @@ +use std::fmt::Display; + +use chrono::{DateTime, TimeZone}; + use crate::currency::SymbolPair; use crate::BoxError; -use chrono::{DateTime, TimeZone}; -use std::fmt::Display; +use std::hash::{Hash, Hasher}; /*************** * Prices @@ -26,7 +29,7 @@ pub struct PriceTicker { ***************/ #[derive(Clone, Debug)] -pub struct Order { +pub struct ExecutedOrder { pub id: i64, pub group_id: Option, pub client_id: i64, @@ -48,7 +51,13 @@ pub struct Order { pub placed_id: Option, } -#[derive(Copy, Clone, Debug)] +impl Hash for ExecutedOrder { + fn hash(&self, state: &mut H) { + state.write(&self.id.to_le_bytes()) + } +} + +#[derive(Copy, Clone, Debug, Hash)] pub enum OrderKind { Limit, ExchangeLimit, @@ -73,11 +82,11 @@ pub struct OrderForm { /// EXCHANGE FOK, IOC, EXCHANGE IOC kind: OrderKind, /// Symbol for desired pair - symbol: SymbolPair, + pair: SymbolPair, /// Price of order - price: String, + price: f64, /// Amount of order (positive for buy, negative for sell) - amount: String, + amount: f64, /// Set the leverage for a derivative order, supported by derivative symbol orders only. /// The value should be between 1 and 100 inclusive. /// The field is optional, if omitted the default leverage value of 10 will be used. @@ -91,12 +100,12 @@ pub struct OrderForm { } impl OrderForm { - pub fn new(symbol: &SymbolPair, price: f64, amount: f64, kind: OrderKind) -> Self { + pub fn new(pair: &SymbolPair, price: f64, amount: f64, kind: OrderKind) -> Self { OrderForm { kind, - symbol: symbol.clone(), - price: price.to_string(), - amount: amount.to_string(), + pair: pair.clone(), + price, + amount, leverage: None, price_trailing: None, price_aux_limit: None, @@ -119,7 +128,7 @@ impl OrderForm { } } - pub fn price_aux_limit(mut self, limit: f64) -> Result { + pub fn with_price_aux_limit(mut self, limit: f64) -> Result { match self.kind { OrderKind::StopLimit | OrderKind::ExchangeStopLimit => { self.price_aux_limit = Some(limit.to_string()); @@ -136,13 +145,38 @@ impl OrderForm { self.tif = Some(tif.format("%Y-%m-%d %H:%M:%S").to_string()); self } + + pub fn kind(&self) -> OrderKind { + self.kind + } + pub fn pair(&self) -> &SymbolPair { + &self.pair + } + pub fn price(&self) -> &f64 { + &self.price + } + pub fn amount(&self) -> &f64 { + &self.amount + } + pub fn leverage(&self) -> Option { + self.leverage + } + pub fn price_trailing(&self) -> &Option { + &self.price_trailing + } + pub fn price_aux_limit(&self) -> &Option { + &self.price_aux_limit + } + pub fn tif(&self) -> &Option { + &self.tif + } } /*************** * Positions ***************/ -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Position { pair: SymbolPair, state: PositionState, @@ -219,7 +253,7 @@ impl Position { pub fn price_liq(&self) -> f64 { self.price_liq } - pub fn position_id(&self) -> u64 { + pub fn id(&self) -> u64 { self.position_id } pub fn profit_state(&self) -> Option { @@ -231,8 +265,27 @@ impl Position { pub fn creation_update(&self) -> Option { self.creation_update } + pub fn is_short(&self) -> bool { + self.amount.is_sign_negative() + } + pub fn is_long(&self) -> bool { + self.amount.is_sign_positive() + } } +impl Hash for Position { + fn hash(&self, state: &mut H) { + state.write(&self.id().to_le_bytes()) + } +} + +impl PartialEq for Position { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for Position {} + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum PositionProfitState { Critical, diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index d995b87..8a58c02 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -4,16 +4,20 @@ use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; use crate::events::{Event, EventKind, EventMetadata, SignalKind}; -use crate::managers::PositionManager; -use crate::models::{Position, PositionProfitState}; +use crate::managers::{OrderManager, PositionManager}; +use crate::models::{ExecutedOrder, OrderForm, Position, PositionProfitState}; + +/*************** +* DEFINITIONS +***************/ pub trait PositionStrategy: DynClone { fn name(&self) -> String; fn on_new_tick( &self, - position: &Position, + position: Position, manager: &PositionManager, - ) -> (Position, Vec, Vec); + ) -> (Position, Vec, Option); } impl Debug for dyn PositionStrategy { @@ -22,6 +26,27 @@ impl Debug for dyn PositionStrategy { } } +pub trait OrderStrategy: DynClone { + /// 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: &ExecutedOrder, manager: &mut OrderManager); +} + +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, @@ -53,12 +78,13 @@ impl PositionStrategy for TrailingStop { fn on_new_tick( &self, - position: &Position, + position: Position, manager: &PositionManager, - ) -> (Position, Vec, Vec) { + ) -> (Position, Vec, Option) { let mut signals = vec![]; let pl_perc = TrailingStop::net_pl_percentage(position.pl_perc(), TrailingStop::TAKER_FEE); let events = vec![]; + let mut order = None; let state = { if pl_perc > TrailingStop::GOOD_PROFIT_PERC { @@ -72,24 +98,22 @@ impl PositionStrategy for TrailingStop { } else if TrailingStop::MAX_LOSS_PERC < pl_perc && pl_perc < 0.0 { PositionProfitState::Loss } else { - signals.push(SignalKind::ClosePosition { - position_id: position.position_id(), - }); + signals.push(SignalKind::ClosePosition(position.clone())); PositionProfitState::Critical } }; - let opt_pre_pw = manager.position_previous_tick(position.position_id(), None); - let event_metadata = EventMetadata::new(Some(position.position_id()), None); + 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, signals); + return (new_position, events, order); } } - None => return (new_position, events, signals), + None => return (new_position, events, order), }; let events = { @@ -130,6 +154,23 @@ impl PositionStrategy for TrailingStop { events }; - return (new_position, events, signals); + return (new_position, events, order); + } +} + +#[derive(Clone, Debug)] +pub struct FastOrderStrategy {} + +impl OrderStrategy for FastOrderStrategy { + fn name(&self) -> String { + "Fast order strategy".into() + } + + fn on_update(&self) { + unimplemented!() + } + + fn on_position_close(&self, order: &ExecutedOrder, manager: &mut OrderManager) { + unimplemented!() } }