From 7ba90a72c0eb4de471b1772c00c04ba7e2056815 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Fri, 12 Feb 2021 14:56:30 +0000 Subject: [PATCH 01/19] code reorganization --- rustybot/src/managers.rs | 72 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index bf34e82..e62136b 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -33,40 +33,6 @@ pub struct PriceManager { client: Client, } -pub struct PriceManagerHandle { - sender: Sender, -} - -impl PriceManagerHandle { - async fn run_price_manager(mut manager: PriceManager) { - while let Some(msg) = manager.receiver.recv().await { - manager.handle_message(msg).await.unwrap(); - } - } - - pub fn new(pair: SymbolPair, client: Client) -> Self { - let (sender, receiver) = channel(1); - - let price_manager = PriceManager::new(receiver, pair, client); - tokio::spawn(PriceManagerHandle::run_price_manager(price_manager)); - - Self { sender } - } - - pub async fn update(&mut self, tick: u64) -> Result { - let (send, recv) = oneshot::channel(); - - self.sender - .send(ActorMessage { - message: Message::Update { tick }, - respond_to: send, - }) - .await?; - - Ok(recv.await?) - } -} - impl PriceManager { pub fn new(receiver: Receiver, pair: SymbolPair, client: Client) -> Self { PriceManager { @@ -112,6 +78,40 @@ impl PriceManager { } } +pub struct PriceManagerHandle { + sender: Sender, +} + +impl PriceManagerHandle { + async fn run_price_manager(mut manager: PriceManager) { + while let Some(msg) = manager.receiver.recv().await { + manager.handle_message(msg).await.unwrap(); + } + } + + pub fn new(pair: SymbolPair, client: Client) -> Self { + let (sender, receiver) = channel(1); + + let price_manager = PriceManager::new(receiver, pair, client); + tokio::spawn(PriceManagerHandle::run_price_manager(price_manager)); + + Self { sender } + } + + pub async fn update(&mut self, tick: u64) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: Message::Update { tick }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } +} + #[derive(Clone, Debug)] pub struct PriceEntry { tick: u64, @@ -431,14 +431,14 @@ impl OrderManager { // No open order, undercutting best price with limit order let closing_price = self.best_closing_price(&position, &order_book); - // TODO: hardcoded platform to Margin! + // TODO: hardcoded platform to Derivative! let order_form = OrderForm::new( self.pair.clone(), OrderKind::Limit { price: closing_price, amount: position.amount().neg(), }, - TradingPlatform::Margin, + TradingPlatform::Derivative, ); info!("Submitting {} order", order_form.kind()); From 6c409ac9fd347481bc42b48ae5939f2c5a8fe0f9 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Fri, 12 Feb 2021 14:56:50 +0000 Subject: [PATCH 02/19] added trading fees model --- rustybot/src/models.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 1e137b5..32c8743 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -563,3 +563,15 @@ pub struct Trade { pub fee: OrderFee, pub fee_currency: Symbol, } + +#[derive(Debug)] +pub enum TradingFees { + Maker { + platform: TradingPlatform, + percentage: f64, + }, + Taker { + platform: TradingPlatform, + percentage: f64, + }, +} From 2acc81cd7521912049005a18f04f3bc9452a1670 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Fri, 12 Feb 2021 14:57:23 +0000 Subject: [PATCH 03/19] remodeled percentage calculation in strategy --- rustybot/src/strategy.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index df9a34a..90aabef 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -4,8 +4,10 @@ use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; use log::info; +use crate::connectors::Connector; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::OptionUpdate; +use crate::models::OrderBookEntry::Trading; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, PositionState, TradingPlatform, @@ -74,10 +76,22 @@ pub struct TrailingStop { } impl TrailingStop { - const BREAK_EVEN_PERC: f64 = 0.1; - const MIN_PROFIT_PERC: f64 = 0.5; - const GOOD_PROFIT_PERC: f64 = TrailingStop::MIN_PROFIT_PERC * 1.75; - const MAX_LOSS_PERC: f64 = -4.0; + // in percentage + const CAPITAL_MAX_LOSS: f64 = 17.5; + const CAPITAL_MIN_PROFIT: f64 = 10.0; + const CAPITAL_GOOD_PROFIT: f64 = 20.0; + + // in percentage + const MIN_PROFIT_TRAILING_DELTA: f64 = 0.2; + const GOOD_PROFIT_TRAILING_DELTA: f64 = 0.1; + + const LEVERAGE: f64 = 15.0; + + const MIN_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_MIN_PROFIT / TrailingStop::LEVERAGE) + + TrailingStop::MIN_PROFIT_TRAILING_DELTA; + const GOOD_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_GOOD_PROFIT / TrailingStop::LEVERAGE) + + TrailingStop::GOOD_PROFIT_TRAILING_DELTA; + const MAX_LOSS_PERC: f64 = -(TrailingStop::CAPITAL_MAX_LOSS / TrailingStop::LEVERAGE); pub fn new() -> Self { TrailingStop { @@ -88,8 +102,8 @@ impl TrailingStop { 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(0.2), - PositionProfitState::Profit => Some(0.1), + PositionProfitState::MinimumProfit => Some(TrailingStop::MIN_PROFIT_TRAILING_DELTA), + PositionProfitState::Profit => Some(TrailingStop::GOOD_PROFIT_TRAILING_DELTA), _ => None, }; @@ -252,7 +266,7 @@ pub struct FastOrderStrategy { impl Default for FastOrderStrategy { fn default() -> Self { - Self { threshold: 0.2 } + Self { threshold: 0.15 } } } From 613b31463130f694f88a75cbf6b3c2d408f7f7dd Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Fri, 12 Feb 2021 14:57:37 +0000 Subject: [PATCH 04/19] added derivative symbols --- rustybot/src/currency.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rustybot/src/currency.rs b/rustybot/src/currency.rs index 6f1d4a1..58d4f4c 100644 --- a/rustybot/src/currency.rs +++ b/rustybot/src/currency.rs @@ -28,6 +28,10 @@ impl Symbol { pub const LTC: Symbol = Symbol::new_static("LTC"); pub const DOT: Symbol = Symbol::new_static("DOT"); + pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0"); + pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0"); + pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0"); + // Paper trading pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); pub const TESTUSD: Symbol = Symbol::new_static("TESTUSD"); From e21080898337b66f2cbed9cd91b2b670cd01bfb0 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Fri, 12 Feb 2021 14:58:43 +0000 Subject: [PATCH 05/19] added support for derivative trading in connectors. stub for fees calculation. hardcoded leverage 15 in order submission --- rustybot/src/connectors.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 26aacfa..4757f7f 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -17,7 +17,7 @@ use tokio::time::Duration; use crate::currency::{Symbol, SymbolPair}; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, - PositionState, PriceTicker, Trade, TradingPlatform, WalletKind, + PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind, }; use crate::BoxError; @@ -69,9 +69,9 @@ impl Client { // TODO: change fee with account's taker fee positions.iter_mut().flatten().for_each(|x| { if x.is_short() { - x.update_profit_loss(best_ask, 0.2); + x.update_profit_loss(best_ask, 0.075); } else { - x.update_profit_loss(best_bid, 0.2); + x.update_profit_loss(best_bid, 0.075); } }); @@ -153,6 +153,7 @@ pub trait Connector: Send + Sync { &self, pair: &SymbolPair, ) -> Result>, BoxError>; + async fn trading_fees(&self) -> Result, BoxError>; } impl Debug for dyn Connector { @@ -189,7 +190,11 @@ impl BitfinexConnector { if pair.to_string().to_lowercase().contains("test") { format!("{}:{}", pair.base(), pair.quote()) } else { - format!("{}{}", pair.base(), pair.quote()) + if pair.to_string().to_lowercase().contains("f0") { + format!("{}:{}", pair.base(), pair.quote()) + } else { + format!("{}{}", pair.base(), pair.quote()) + } } } @@ -279,9 +284,11 @@ impl Connector for BitfinexConnector { let order_form = match order.kind() { OrderKind::Limit { price, amount } => { bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + .with_leverage(15) } - OrderKind::Market { amount } => { + OrderKind::Market { amount, .. } => { bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) + .with_leverage(15) } OrderKind::Stop { price, amount } => { bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) @@ -371,6 +378,10 @@ impl Connector for BitfinexConnector { Ok((!mapped_vec.is_empty()).then_some(mapped_vec)) } + + async fn trading_fees(&self) -> Result, BoxError> { + unimplemented!() + } } impl From<&ActiveOrder> for CancelOrderForm { @@ -439,7 +450,7 @@ impl From<&OrderForm> for bitfinex::orders::OrderKind { OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::ExchangeFok, OrderKind::ImmediateOrCancel { .. } => bitfinex::orders::OrderKind::ExchangeIoc, }, - TradingPlatform::Margin => match o.kind() { + TradingPlatform::Margin | TradingPlatform::Derivative => match o.kind() { OrderKind::Limit { .. } => bitfinex::orders::OrderKind::Limit, OrderKind::Market { .. } => bitfinex::orders::OrderKind::Market, OrderKind::Stop { .. } => bitfinex::orders::OrderKind::Stop, From b46aec3395dc936c54fe54ab2db8859e3292ea14 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 13 Feb 2021 13:49:06 +0000 Subject: [PATCH 06/19] use dotenv --- rustybot/Cargo.lock | 194 +++++++++++-------------------------------- rustybot/Cargo.toml | 3 +- rustybot/src/main.rs | 36 ++++---- 3 files changed, 66 insertions(+), 167 deletions(-) diff --git a/rustybot/Cargo.lock b/rustybot/Cargo.lock index cba87b0..82c4d3c 100644 --- a/rustybot/Cargo.lock +++ b/rustybot/Cargo.lock @@ -87,12 +87,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - [[package]] name = "base64" version = "0.12.3" @@ -121,7 +115,7 @@ dependencies = [ "serde_derive", "serde_json", "tokio 1.1.0", - "tungstenite 0.9.2", + "tungstenite 0.12.0", "url", ] @@ -131,34 +125,13 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.3", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array", ] [[package]] @@ -167,28 +140,12 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "0.5.6" @@ -271,24 +228,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.3", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dyn-clone" version = "1.0.4" @@ -314,12 +268,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fern" version = "0.6.0" @@ -476,15 +424,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -534,7 +473,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.2", + "http", "indexmap", "slab", "tokio 1.1.0", @@ -564,17 +503,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa", -] - [[package]] name = "http" version = "0.2.2" @@ -593,7 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" dependencies = [ "bytes 1.0.1", - "http 0.2.2", + "http", ] [[package]] @@ -619,7 +547,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.2", + "http", "http-body", "httparse", "httpdate", @@ -666,15 +594,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "input_buffer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1b822cc844905551931d6f81608ed5f50a79c1078a4e2b4d42dbc7c1eedfbf" -dependencies = [ - "bytes 0.4.12", -] - [[package]] name = "input_buffer" version = "0.3.1" @@ -684,6 +603,15 @@ dependencies = [ "bytes 0.5.6", ] +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes 1.0.1", +] + [[package]] name = "instant" version = "0.1.9" @@ -693,15 +621,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" version = "2.3.0" @@ -903,12 +822,6 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -1222,7 +1135,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http 0.2.2", + "http", "http-body", "hyper", "hyper-tls", @@ -1274,6 +1187,7 @@ dependencies = [ "bitfinex", "byteorder", "chrono", + "dotenv", "dyn-clone", "fern", "float-cmp", @@ -1371,29 +1285,17 @@ dependencies = [ "serde", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if 1.0.0", "cpuid-bool", - "digest 0.9.0", - "opaque-debug 0.3.0", + "digest", + "opaque-debug", ] [[package]] @@ -1631,26 +1533,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" -[[package]] -name = "tungstenite" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0c2bd5aeb7dcd2bb32e472c8872759308495e5eccc942e929a513cd8d36110" -dependencies = [ - "base64 0.11.0", - "byteorder", - "bytes 0.4.12", - "http 0.1.21", - "httparse", - "input_buffer 0.2.0", - "log 0.4.11", - "native-tls", - "rand 0.7.3", - "sha-1 0.8.2", - "url", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.11.1" @@ -1660,12 +1542,32 @@ dependencies = [ "base64 0.12.3", "byteorder", "bytes 0.5.6", - "http 0.2.2", + "http", "httparse", "input_buffer 0.3.1", "log 0.4.11", "rand 0.7.3", - "sha-1 0.9.2", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64 0.13.0", + "byteorder", + "bytes 1.0.1", + "http", + "httparse", + "input_buffer 0.4.0", + "log 0.4.11", + "native-tls", + "rand 0.8.2", + "sha-1", "url", "utf-8", ] diff --git a/rustybot/Cargo.toml b/rustybot/Cargo.toml index b22004a..5f31416 100644 --- a/rustybot/Cargo.toml +++ b/rustybot/Cargo.toml @@ -20,4 +20,5 @@ chrono = "0.4" byteorder = "1" float-cmp = "0.8" merge = "0.1" -futures-retry = "0.6" \ No newline at end of file +futures-retry = "0.6" +dotenv = "0.15" \ No newline at end of file diff --git a/rustybot/src/main.rs b/rustybot/src/main.rs index 8a0d3e5..3f76b66 100644 --- a/rustybot/src/main.rs +++ b/rustybot/src/main.rs @@ -1,6 +1,8 @@ #![feature(drain_filter)] #![feature(bool_to_option)] +use std::env; + use fern::colors::{Color, ColoredLevelConfig}; use log::LevelFilter::{Debug, Trace}; use tokio::time::Duration; @@ -23,33 +25,27 @@ pub type BoxError = Box; #[tokio::main] async fn main() -> Result<(), BoxError> { setup_logger()?; + dotenv::dotenv()?; - // TEST - let test_api_key = "P1EVE68DJByDAkGQvpIkTwfrbYXd2Vo2ZaIhTYb9vx2"; - let test_api_secret = "1nicg8z0zKVEt5Rb7ZDpIYjVYVTgvCaCPMZqB0niFli"; - - // REAL - // let orders_api_key = "hc5nDvYbFYJZMKdnzYq8P4AzCSwjxfQHnMyrg69Sf4c"; - // let orders_api_secret = "53x9goIOpbOtBoPi7dmigK5Cq5e0282EUO2qRIMEXlh"; - // let prices_api_key = "gTfFZUCwRBE0Z9FZjyk9HNe4lZ7XuiZY9rrW71SyUr9"; - // let prices_api_secret = "zWbxvoFZad3BPIiXK4DKfEvC0YsAuaApbeAyI8OBXgN"; - // let positions_api_key = "PfR7BadPZPNdVZnkHFBfAjsg7gjt8pAecMj5B8eRPFi"; - // let positions_api_secret = "izzvxtE3XsBBRpVCHGJ8f60UA56SmPNbBvJGVd67aqD"; + let api_key = env::vars() + .find(|(k, v)| k == "API_KEY") + .map(|(k, v)| v) + .ok_or("API_KEY not set!")?; + let api_secret = env::vars() + .find(|(k, v)| k == "API_SECRET") + .map(|(k, v)| v) + .ok_or("API_SECRET not set!")?; let bitfinex = ExchangeDetails::Bitfinex { - prices_api_key: test_api_key.into(), - prices_api_secret: test_api_secret.into(), - orders_api_key: test_api_key.into(), - orders_api_secret: test_api_secret.into(), - positions_api_key: test_api_key.into(), - positions_api_secret: test_api_secret.into(), + api_key: api_key.into(), + api_secret: api_secret.into(), }; let mut bot = BfxBot::new( vec![bitfinex], - vec![Symbol::BTC, Symbol::ETH, Symbol::XMR], - Symbol::USD, - Duration::new(1, 0), + vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC], + Symbol::DERIV_USDT, + Duration::new(10, 0), ); Ok(bot.start_loop().await?) From ce8eec71ff2aa56831cc1cb86ae7fb6f22b6992d Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 13 Feb 2021 14:58:15 +0000 Subject: [PATCH 07/19] fetch account fees on-demand and apply them to position p/l --- rustybot/src/connectors.rs | 111 +++++++++++++++++++++++++++++++------ rustybot/src/models.rs | 8 ++- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 4757f7f..0a32b6c 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -57,21 +57,52 @@ impl Client { pair: &SymbolPair, ) -> Result>, BoxError> { // retrieving open positions and order book to calculate effective profit/loss - let (positions, order_book) = tokio::join!( + let (positions, order_book, fees) = tokio::join!( self.inner.active_positions(pair), - self.inner.order_book(pair) + self.inner.order_book(pair), + self.inner.trading_fees() ); - let (mut positions, order_book) = (positions?, order_book?); + let (mut positions, order_book, fees) = (positions?, order_book?, fees?); let (best_ask, best_bid) = (order_book.lowest_ask(), order_book.highest_bid()); + let derivative_taker = fees + .iter() + .filter_map(|x| match x { + TradingFees::Taker { + platform, + percentage, + } if platform == &TradingPlatform::Derivative => Some(percentage), + _ => None, + }) + .next() + .ok_or("Could not retrieve derivative taker fee!")?; + let margin_taker = fees + .iter() + .filter_map(|x| match x { + TradingFees::Taker { + platform, + percentage, + } if platform == &TradingPlatform::Margin => Some(percentage), + _ => None, + }) + .next() + .ok_or("Could not retrieve margin taker fee!")?; + // updating positions with effective profit/loss - // TODO: change fee with account's taker fee positions.iter_mut().flatten().for_each(|x| { + let fee = match x.platform() { + TradingPlatform::Funding | TradingPlatform::Exchange => { + unimplemented!() + } + TradingPlatform::Margin => margin_taker, + TradingPlatform::Derivative => derivative_taker, + }; + if x.is_short() { - x.update_profit_loss(best_ask, 0.075); + x.update_profit_loss(best_ask, *fee); } else { - x.update_profit_loss(best_bid, 0.075); + x.update_profit_loss(best_bid, *fee); } }); @@ -380,7 +411,46 @@ impl Connector for BitfinexConnector { } async fn trading_fees(&self) -> Result, BoxError> { - unimplemented!() + let mut fees = vec![]; + let accountfees = self.bfx.account.account_summary().await?; + + // Derivatives + let derivative_taker = TradingFees::Taker { + platform: TradingPlatform::Derivative, + percentage: accountfees.derivative_taker() * 100.0, + }; + let derivative_maker = TradingFees::Maker { + platform: TradingPlatform::Derivative, + percentage: accountfees.derivative_rebate() * 100.0, + }; + fees.push(derivative_taker); + fees.push(derivative_maker); + + // Exchange + let exchange_taker = TradingFees::Taker { + platform: TradingPlatform::Exchange, + percentage: accountfees.taker_to_fiat() * 100.0, + }; + let exchange_maker = TradingFees::Maker { + platform: TradingPlatform::Exchange, + percentage: accountfees.maker_fee() * 100.0, + }; + fees.push(exchange_taker); + fees.push(exchange_maker); + + // Margin + let margin_taker = TradingFees::Taker { + platform: TradingPlatform::Margin, + percentage: accountfees.taker_to_fiat() * 100.0, + }; + let margin_maker = TradingFees::Maker { + platform: TradingPlatform::Margin, + percentage: accountfees.maker_fee() * 100.0, + }; + fees.push(margin_taker); + fees.push(margin_maker); + + Ok(fees) } } @@ -423,6 +493,14 @@ impl TryInto for bitfinex::positions::Position { } }; + let platform = { + if self.symbol().to_ascii_lowercase().contains("f0") { + TradingPlatform::Derivative + } else { + TradingPlatform::Margin + } + }; + Ok(Position::new( SymbolPair::from_str(self.symbol())?, state, @@ -432,6 +510,7 @@ impl TryInto for bitfinex::positions::Position { self.pl_perc(), self.price_liq(), self.position_id(), + platform, ) .with_creation_date(self.mts_create()) .with_creation_update(self.mts_update())) @@ -648,23 +727,23 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderDetails { // TODO: fields are hardcoded, to fix impl From<&bitfinex::responses::TradeResponse> for Trade { fn from(response: &TradeResponse) -> Self { - let pair = SymbolPair::from_str(&response.symbol).unwrap(); + let pair = SymbolPair::from_str(&response.symbol()).unwrap(); let fee = { - if response.is_maker { - OrderFee::Maker(response.fee) + if response.is_maker() { + OrderFee::Maker(response.fee()) } else { - OrderFee::Taker(response.fee) + OrderFee::Taker(response.fee()) } }; Self { - trade_id: response.trade_id, + trade_id: response.trade_id(), pair, - execution_timestamp: response.execution_timestamp, - price: response.execution_price, - amount: response.execution_amount, + execution_timestamp: response.execution_timestamp(), + price: response.execution_price(), + amount: response.execution_amount(), fee, - fee_currency: Symbol::new(response.symbol.clone()), + fee_currency: Symbol::new(response.symbol().to_owned().clone()), } } } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 32c8743..305d52b 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -186,7 +186,7 @@ impl PartialEq for ActiveOrder { impl Eq for ActiveOrder {} -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum TradingPlatform { Exchange, Derivative, @@ -396,6 +396,7 @@ pub struct Position { position_id: u64, creation_date: Option, creation_update: Option, + platform: TradingPlatform, } impl Position { @@ -408,6 +409,7 @@ impl Position { pl_perc: f64, price_liq: f64, position_id: u64, + platform: TradingPlatform, ) -> Self { Position { pair, @@ -421,6 +423,7 @@ impl Position { creation_date: None, creation_update: None, profit_state: None, + platform, } } @@ -505,6 +508,9 @@ impl Position { pub fn is_long(&self) -> bool { self.amount.is_sign_positive() } + pub fn platform(&self) -> TradingPlatform { + self.platform + } } impl Hash for Position { From 127ffaa1b958fef2faecc12df74a564cb2fc6a5c Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 13 Feb 2021 15:29:00 +0000 Subject: [PATCH 08/19] moved amount out of OrderKind and into OrderForm. leverage detection for open positions --- rustybot/src/connectors.rs | 93 ++++++++++++------------ rustybot/src/managers.rs | 5 +- rustybot/src/models.rs | 140 +++++++++++++------------------------ rustybot/src/strategy.rs | 7 +- 4 files changed, 99 insertions(+), 146 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 0a32b6c..f1da7ef 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -311,39 +311,46 @@ impl Connector for BitfinexConnector { async fn submit_order(&self, order: &OrderForm) -> Result { let symbol_name = format!("t{}", BitfinexConnector::format_trading_pair(order.pair())); + let amount = order.amount(); - let order_form = match order.kind() { - OrderKind::Limit { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - .with_leverage(15) + let order_form = { + let pre_leverage = { + match order.kind() { + OrderKind::Limit { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::Market => { + bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) + } + OrderKind::Stop { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::StopLimit { price, limit_price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + .with_price_aux_limit(limit_price)? + } + OrderKind::TrailingStop { distance } => { + bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) + .with_price_trailing(distance)? + } + OrderKind::FillOrKill { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::ImmediateOrCancel { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + } + .with_meta(OrderMeta::new( + BitfinexConnector::AFFILIATE_CODE.to_string(), + )) + }; + + // adding leverage, if any + match order.leverage() { + Some(leverage) => pre_leverage.with_leverage(leverage as u32), + None => pre_leverage, } - OrderKind::Market { amount, .. } => { - bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) - .with_leverage(15) - } - OrderKind::Stop { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - OrderKind::StopLimit { - price, - amount, - limit_price, - } => bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - .with_price_aux_limit(limit_price)?, - OrderKind::TrailingStop { distance, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) - .with_price_trailing(distance)? - } - OrderKind::FillOrKill { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - OrderKind::ImmediateOrCancel { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - } - .with_meta(OrderMeta::new( - BitfinexConnector::AFFILIATE_CODE.to_string(), - )); + }; let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?; @@ -474,6 +481,7 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder { SymbolPair::from_str(response.symbol())?, response.into(), response.into(), + response.amount(), ), creation_timestamp: 0, update_timestamp: 0, @@ -511,6 +519,7 @@ impl TryInto for bitfinex::positions::Position { self.price_liq(), self.position_id(), platform, + self.leverage(), ) .with_creation_date(self.mts_create()) .with_creation_update(self.mts_update())) @@ -579,41 +588,33 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => { Self::Limit { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { - Self::Market { - amount: response.amount(), - } + Self::Market } bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { Self::Stop { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { price: response.price(), - amount: response.amount(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { distance: response.price_trailing().expect("Distance not found!"), - amount: response.amount(), }, bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { Self::FillOrKill { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { Self::ImmediateOrCancel { price: response.price(), - amount: response.amount(), } } } @@ -626,41 +627,33 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => { Self::Limit { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { - Self::Market { - amount: response.amount(), - } + Self::Market {} } bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { Self::Stop { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { price: response.price(), - amount: response.amount(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { distance: response.price_trailing().expect("Distance not found!"), - amount: response.amount(), }, bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { Self::FillOrKill { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { Self::ImmediateOrCancel { price: response.price(), - amount: response.amount(), } } } @@ -677,7 +670,7 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder { group_id: order.group_id().map(|x| x as u64), client_id: Some(order.client_id()), symbol: pair.clone(), - details: OrderForm::new(pair, order.into(), order.into()), + details: OrderForm::new(pair, order.into(), order.into(), order.amount()), creation_timestamp: order.creation_timestamp(), update_timestamp: order.update_timestamp(), } diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index e62136b..6802f8d 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -436,10 +436,11 @@ impl OrderManager { self.pair.clone(), OrderKind::Limit { price: closing_price, - amount: position.amount().neg(), }, TradingPlatform::Derivative, - ); + position.amount().neg(), + ) + .with_leverage(position.leverage()); info!("Submitting {} order", order_form.kind()); if let Err(e) = self.client.submit_order(&order_form).await { diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 305d52b..2b6d064 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -213,34 +213,13 @@ impl Display for TradingPlatform { #[derive(Copy, Clone, Debug)] pub enum OrderKind { - Limit { - price: f64, - amount: f64, - }, - Market { - amount: f64, - }, - Stop { - price: f64, - amount: f64, - }, - StopLimit { - price: f64, - amount: f64, - limit_price: f64, - }, - TrailingStop { - distance: f64, - amount: f64, - }, - FillOrKill { - price: f64, - amount: f64, - }, - ImmediateOrCancel { - price: f64, - amount: f64, - }, + Limit { price: f64 }, + Market, + Stop { price: f64 }, + StopLimit { price: f64, limit_price: f64 }, + TrailingStop { distance: f64 }, + FillOrKill { price: f64 }, + ImmediateOrCancel { price: f64 }, } impl OrderKind { @@ -260,67 +239,32 @@ impl OrderKind { impl Display for OrderKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - OrderKind::Limit { price, amount } => { + OrderKind::Limit { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) + } + OrderKind::Market => { + write!(f, "[{}]", self.as_str()) + } + OrderKind::Stop { price } => { + write!(f, "[{} | Price: {:0.5}", self.as_str(), price,) + } + OrderKind::StopLimit { price, limit_price } => { write!( f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", + "[{} | Price: {:0.5}, Limit Price: {:0.5}]", self.as_str(), price, - amount - ) - } - OrderKind::Market { amount } => { - write!(f, "[{} | Amount: {:0.5}]", self.as_str(), amount) - } - OrderKind::Stop { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) - } - OrderKind::StopLimit { - price, - amount, - limit_price, - } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}, Limit Price: {:0.5}]", - self.as_str(), - price, - amount, limit_price ) } - OrderKind::TrailingStop { distance, amount } => { - write!( - f, - "[{} | Distance: {:0.5}, Amount: {:0.5}]", - self.as_str(), - distance, - amount - ) + OrderKind::TrailingStop { distance } => { + write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,) } - OrderKind::FillOrKill { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) + OrderKind::FillOrKill { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) } - OrderKind::ImmediateOrCancel { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) + OrderKind::ImmediateOrCancel { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) } } } @@ -331,17 +275,31 @@ pub struct OrderForm { pair: SymbolPair, kind: OrderKind, platform: TradingPlatform, + amount: f64, + leverage: Option, } impl OrderForm { - pub fn new(pair: SymbolPair, order_kind: OrderKind, platform: TradingPlatform) -> Self { + pub fn new( + pair: SymbolPair, + order_kind: OrderKind, + platform: TradingPlatform, + amount: f64, + ) -> Self { Self { pair, kind: order_kind, platform, + amount, + leverage: None, } } + pub fn with_leverage(mut self, leverage: f64) -> Self { + self.leverage = Some(leverage); + self + } + pub fn pair(&self) -> &SymbolPair { &self.pair } @@ -355,15 +313,7 @@ impl OrderForm { } pub fn amount(&self) -> f64 { - match self.kind { - OrderKind::Limit { amount, .. } => amount, - OrderKind::Market { amount } => amount, - OrderKind::Stop { amount, .. } => amount, - OrderKind::StopLimit { amount, .. } => amount, - OrderKind::TrailingStop { amount, .. } => amount, - OrderKind::FillOrKill { amount, .. } => amount, - OrderKind::ImmediateOrCancel { amount, .. } => amount, - } + self.amount } pub fn price(&self) -> Option { @@ -377,6 +327,10 @@ impl OrderForm { OrderKind::ImmediateOrCancel { price, .. } => Some(price), } } + + pub fn leverage(&self) -> Option { + self.leverage + } } /*************** @@ -397,6 +351,7 @@ pub struct Position { creation_date: Option, creation_update: Option, platform: TradingPlatform, + leverage: f64, } impl Position { @@ -410,6 +365,7 @@ impl Position { price_liq: f64, position_id: u64, platform: TradingPlatform, + leverage: f64, ) -> Self { Position { pair, @@ -424,6 +380,7 @@ impl Position { creation_update: None, profit_state: None, platform, + leverage, } } @@ -511,6 +468,9 @@ impl Position { pub fn platform(&self) -> TradingPlatform { self.platform } + pub fn leverage(&self) -> f64 { + self.leverage + } } impl Hash for Position { diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 90aabef..a7c2064 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -10,7 +10,7 @@ use crate::managers::OptionUpdate; use crate::models::OrderBookEntry::Trading; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, - PositionState, TradingPlatform, + PositionState, TradingFees, TradingPlatform, }; use crate::BoxError; @@ -309,10 +309,9 @@ impl OrderStrategy for FastOrderStrategy { messages.push(Message::SubmitOrder { order: OrderForm::new( order.symbol.clone(), - OrderKind::Market { - amount: order.details.amount(), - }, + OrderKind::Market {}, order.details.platform().clone(), + order.details.amount(), ), }) } From 597dc57bd5c811f94b53c8886e97119eedca84e6 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 13 Feb 2021 15:31:19 +0000 Subject: [PATCH 09/19] cargo fix --- rustybot/src/strategy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index a7c2064..af84cd0 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -7,7 +7,7 @@ use log::info; use crate::connectors::Connector; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::OptionUpdate; -use crate::models::OrderBookEntry::Trading; + use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, PositionState, TradingFees, TradingPlatform, From e1330928310ffd96de3797ded4b46eee9401ce81 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 13 Feb 2021 15:52:50 +0000 Subject: [PATCH 10/19] Thank you Clippy! --- rustybot/src/connectors.rs | 14 ++++---- rustybot/src/managers.rs | 72 ++++++++++++++------------------------ rustybot/src/models.rs | 23 ------------ rustybot/src/strategy.rs | 33 +++++------------ 4 files changed, 42 insertions(+), 100 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index f1da7ef..3d18dcf 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -208,7 +208,7 @@ impl BitfinexConnector { if e.to_string().contains("nonce: small") { return RetryPolicy::WaitRetry(Duration::from_millis(1)); } - return RetryPolicy::ForwardError(e); + RetryPolicy::ForwardError(e) } pub fn new(api_key: &str, api_secret: &str) -> Self { @@ -218,14 +218,12 @@ impl BitfinexConnector { } fn format_trading_pair(pair: &SymbolPair) -> String { - if pair.to_string().to_lowercase().contains("test") { + if pair.to_string().to_lowercase().contains("test") + || pair.to_string().to_lowercase().contains("f0") + { format!("{}:{}", pair.base(), pair.quote()) } else { - if pair.to_string().to_lowercase().contains("f0") { - format!("{}:{}", pair.base(), pair.quote()) - } else { - format!("{}{}", pair.base(), pair.quote()) - } + format!("{}{}", pair.base(), pair.quote()) } } @@ -736,7 +734,7 @@ impl From<&bitfinex::responses::TradeResponse> for Trade { price: response.execution_price(), amount: response.execution_amount(), fee, - fee_currency: Symbol::new(response.symbol().to_owned().clone()), + fee_currency: Symbol::new(response.symbol().to_owned()), } } } diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index 6802f8d..77fe2c1 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -44,12 +44,9 @@ impl PriceManager { } pub async fn handle_message(&mut self, message: ActorMessage) -> Result<(), BoxError> { - match message.message { - Message::Update { tick } => { - let a = self.update(tick).await?; - self.add_entry(a); - } - _ => {} + if let Message::Update { tick } = message.message { + let a = self.update(tick).await?; + self.add_entry(a); } Ok(message @@ -247,11 +244,7 @@ impl PositionManager { Some(positions) => { // checking if there are positions open for our pair - match positions - .into_iter() - .filter(|x| x.pair() == &self.pair) - .next() - { + match positions.into_iter().find(|x| x.pair() == &self.pair) { // no open positions for our pair, setting active position to none None => { self.active_position = None; @@ -300,10 +293,7 @@ impl PositionManager { None => self.current_tick() - 1, }; - self.positions_history - .get(&tick) - .filter(|x| x.id() == id) - .and_then(|x| Some(x)) + self.positions_history.get(&tick).filter(|x| x.id() == id) } } @@ -421,13 +411,14 @@ impl OrderManager { if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) { let opt_position_order = open_orders .iter() - .find(|x| x.details.amount().neg() == position.amount()); + // avoid using direct equality, using error margin instead + .find(|x| (x.details.amount().neg() - position.amount()).abs() < 0.0000001); // checking if the position has an open order. // If so, don't do anything since the order is taken care of // in the update phase. // If no order is open, send an undercut limit order at the best current price. - if let None = opt_position_order { + if opt_position_order.is_none() { // No open order, undercutting best price with limit order let closing_price = self.best_closing_price(&position, &order_book); @@ -509,26 +500,22 @@ impl OrderManager { let delta = (ask - bid) / 10.0; let closing_price = { - let closing_price = { - if position.is_short() { - bid - delta - } else { - ask + delta - } - }; - - if avg > 9999.0 { - if position.is_short() { - closing_price.ceil() - } else { - closing_price.floor() - } + if position.is_short() { + bid - delta } else { - closing_price + ask + delta } }; - closing_price + if avg > 9999.0 { + if position.is_short() { + closing_price.ceil() + } else { + closing_price.floor() + } + } else { + closing_price + } } } @@ -550,8 +537,8 @@ impl PairManager { Box::new(FastOrderStrategy::default()), ), position_manager: PositionManagerHandle::new( - pair.clone(), - client.clone(), + pair, + client, Box::new(TrailingStop::new()), ), } @@ -578,11 +565,8 @@ impl PairManager { // TODO: to move into Handler? if let Some(messages) = messages { for m in messages { - match m { - Message::ClosePosition { position_id } => { - self.order_manager.close_position(position_id).await?; - } - _ => {} + if let Message::ClosePosition { position_id } = m { + self.order_manager.close_position(position_id).await?; } } } @@ -594,21 +578,19 @@ impl PairManager { pub struct ExchangeManager { kind: ExchangeDetails, pair_managers: Vec, - client: Client, } impl ExchangeManager { - pub fn new(kind: &ExchangeDetails, pairs: &Vec) -> Self { + pub fn new(kind: &ExchangeDetails, pairs: &[SymbolPair]) -> Self { let client = Client::new(kind); let pair_managers = pairs - .into_iter() + .iter() .map(|x| PairManager::new(x.clone(), client.clone())) .collect(); Self { kind: kind.clone(), pair_managers, - client, } } @@ -620,7 +602,7 @@ impl ExchangeManager { .collect(); // execute the futures - while let Some(_) = futures.next().await {} + while futures.next().await.is_some() {} Ok(()) } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 2b6d064..8873578 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -139,18 +139,6 @@ impl OrderDetails { } } - pub fn exchange(&self) -> Exchange { - self.exchange - } - pub fn platform(&self) -> TradingPlatform { - self.platform - } - pub fn kind(&self) -> OrderKind { - self.kind - } - pub fn execution_timestamp(&self) -> u64 { - self.execution_timestamp - } pub fn id(&self) -> u64 { self.id } @@ -496,17 +484,6 @@ pub enum PositionProfitState { Profit, } -impl PositionProfitState { - fn color(self) -> String { - match self { - PositionProfitState::Critical | PositionProfitState::Loss => "red", - PositionProfitState::BreakEven => "yellow", - PositionProfitState::MinimumProfit | PositionProfitState::Profit => "green", - } - .into() - } -} - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum PositionState { Closed, diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index af84cd0..374ad62 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -7,11 +7,7 @@ use log::info; use crate::connectors::Connector; use crate::events::{Event, EventKind, EventMetadata, Message}; use crate::managers::OptionUpdate; - -use crate::models::{ - ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, - PositionState, TradingFees, TradingPlatform, -}; +use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; use crate::BoxError; /*************** @@ -159,9 +155,9 @@ impl PositionStrategy for TrailingStop { && pl_perc < TrailingStop::GOOD_PROFIT_PERC { PositionProfitState::MinimumProfit - } else if 0.0 <= pl_perc && pl_perc < TrailingStop::MIN_PROFIT_PERC { + } else if (0.0..TrailingStop::MIN_PROFIT_PERC).contains(&pl_perc) { PositionProfitState::BreakEven - } else if TrailingStop::MAX_LOSS_PERC < pl_perc && pl_perc < 0.0 { + } else if (TrailingStop::MAX_LOSS_PERC..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical @@ -170,7 +166,7 @@ impl PositionStrategy for TrailingStop { 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)); + let new_position = position.with_profit_state(Some(state)); match opt_prev_position { Some(prev) => { @@ -219,7 +215,7 @@ impl PositionStrategy for TrailingStop { events }; - return (new_position, Some(events), None); + (new_position, Some(events), None) } fn post_tick( @@ -233,14 +229,9 @@ impl PositionStrategy for TrailingStop { }; // if critical, early return with close position - if let Some(profit_state) = position.profit_state() { - match profit_state { - PositionProfitState::Critical => { - info!("Maximum loss reached. Closing position."); - return (position, None, Some(vec![close_message])); - } - _ => {} - } + 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 @@ -270,12 +261,6 @@ impl Default for FastOrderStrategy { } } -impl FastOrderStrategy { - pub fn new(threshold: f64) -> Self { - Self { threshold } - } -} - impl OrderStrategy for FastOrderStrategy { fn name(&self) -> String { "Fast order strategy".into() @@ -310,7 +295,7 @@ impl OrderStrategy for FastOrderStrategy { order: OrderForm::new( order.symbol.clone(), OrderKind::Market {}, - order.details.platform().clone(), + *order.details.platform(), order.details.amount(), ), }) From 70c2dcde172ad6a8a61eecc962a335824ef61b59 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Tue, 16 Feb 2021 18:17:30 +0000 Subject: [PATCH 11/19] added deriv test usdt/btc --- rustybot/src/currency.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rustybot/src/currency.rs b/rustybot/src/currency.rs index 58d4f4c..bed1115 100644 --- a/rustybot/src/currency.rs +++ b/rustybot/src/currency.rs @@ -36,6 +36,9 @@ impl Symbol { pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); pub const TESTUSD: Symbol = Symbol::new_static("TESTUSD"); + pub const DERIV_TESTBTC: Symbol = Symbol::new_static("TESTBTCF0"); + pub const DERIV_TESTUSDT: Symbol = Symbol::new_static("TESTUSDTF0"); + // Fiat coins pub const USD: Symbol = Symbol::new_static("USD"); pub const GBP: Symbol = Symbol::new_static("GBP"); From 6e848c35e36d337666f2dcf4b3b34c37e0469d09 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Tue, 16 Feb 2021 18:17:48 +0000 Subject: [PATCH 12/19] early exit on empty positions --- rustybot/src/connectors.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 3d18dcf..596ece5 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -66,6 +66,10 @@ impl Client { let (mut positions, order_book, fees) = (positions?, order_book?, fees?); let (best_ask, best_bid) = (order_book.lowest_ask(), order_book.highest_bid()); + if positions.is_none() { + return Ok(None); + } + let derivative_taker = fees .iter() .filter_map(|x| match x { @@ -345,7 +349,9 @@ impl Connector for BitfinexConnector { // adding leverage, if any match order.leverage() { - Some(leverage) => pre_leverage.with_leverage(leverage as u32), + // TODO: CHANGEME!!!! + Some(leverage) => pre_leverage.with_leverage(15), + // Some(leverage) => pre_leverage.with_leverage(leverage.round() as u32), None => pre_leverage, } }; @@ -417,7 +423,8 @@ impl Connector for BitfinexConnector { async fn trading_fees(&self) -> Result, BoxError> { let mut fees = vec![]; - let accountfees = self.bfx.account.account_summary().await?; + let accountfees = + BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?; // Derivatives let derivative_taker = TradingFees::Taker { @@ -507,6 +514,7 @@ impl TryInto for bitfinex::positions::Position { } }; + println!("leverage: {}", self.leverage()); Ok(Position::new( SymbolPair::from_str(self.symbol())?, state, From 7f848223b994b2d46163953313908ddf11aed95e Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Tue, 16 Feb 2021 18:18:39 +0000 Subject: [PATCH 13/19] renamed strategies --- rustybot/src/managers.rs | 6 +-- rustybot/src/strategy.rs | 99 ++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 69 deletions(-) diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index 77fe2c1..ed32db7 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -16,7 +16,7 @@ use crate::events::{ActorMessage, Event, Message}; use crate::models::{ ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, TradingPlatform, }; -use crate::strategy::{FastOrderStrategy, OrderStrategy, PositionStrategy, TrailingStop}; +use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::BoxError; pub type OptionUpdate = (Option>, Option>); @@ -534,12 +534,12 @@ impl PairManager { order_manager: OrderManagerHandle::new( pair.clone(), client.clone(), - Box::new(FastOrderStrategy::default()), + Box::new(MarketEnforce::default()), ), position_manager: PositionManagerHandle::new( pair, client, - Box::new(TrailingStop::new()), + Box::new(HiddenTrailingStop::new()), ), } } diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 374ad62..e4fe8da 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -67,15 +67,15 @@ impl Debug for dyn OrderStrategy { ***************/ #[derive(Clone, Debug)] -pub struct TrailingStop { +pub struct HiddenTrailingStop { stop_percentages: HashMap, } -impl TrailingStop { +impl HiddenTrailingStop { // in percentage - const CAPITAL_MAX_LOSS: f64 = 17.5; - const CAPITAL_MIN_PROFIT: f64 = 10.0; - const CAPITAL_GOOD_PROFIT: f64 = 20.0; + const CAPITAL_MAX_LOSS: f64 = 15.0; + const CAPITAL_MIN_PROFIT: f64 = 9.0; + const CAPITAL_GOOD_PROFIT: f64 = HiddenTrailingStop::CAPITAL_MIN_PROFIT * 2.0; // in percentage const MIN_PROFIT_TRAILING_DELTA: f64 = 0.2; @@ -83,14 +83,17 @@ impl TrailingStop { const LEVERAGE: f64 = 15.0; - const MIN_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_MIN_PROFIT / TrailingStop::LEVERAGE) - + TrailingStop::MIN_PROFIT_TRAILING_DELTA; - const GOOD_PROFIT_PERC: f64 = (TrailingStop::CAPITAL_GOOD_PROFIT / TrailingStop::LEVERAGE) - + TrailingStop::GOOD_PROFIT_TRAILING_DELTA; - const MAX_LOSS_PERC: f64 = -(TrailingStop::CAPITAL_MAX_LOSS / TrailingStop::LEVERAGE); + const MIN_PROFIT_PERC: f64 = (HiddenTrailingStop::CAPITAL_MIN_PROFIT + / HiddenTrailingStop::LEVERAGE) + + HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA; + const GOOD_PROFIT_PERC: f64 = (HiddenTrailingStop::CAPITAL_GOOD_PROFIT + / HiddenTrailingStop::LEVERAGE) + + HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA; + const MAX_LOSS_PERC: f64 = + -(HiddenTrailingStop::CAPITAL_MAX_LOSS / HiddenTrailingStop::LEVERAGE); pub fn new() -> Self { - TrailingStop { + HiddenTrailingStop { stop_percentages: HashMap::new(), } } @@ -98,8 +101,10 @@ impl TrailingStop { 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(TrailingStop::MIN_PROFIT_TRAILING_DELTA), - PositionProfitState::Profit => Some(TrailingStop::GOOD_PROFIT_TRAILING_DELTA), + PositionProfitState::MinimumProfit => { + Some(HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA) + } + PositionProfitState::Profit => Some(HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA), _ => None, }; @@ -125,8 +130,10 @@ impl TrailingStop { } } info!( - "\tState: {:?} | PL%: {:0.2} | Stop: {:0.2}", + "\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) ); @@ -134,9 +141,9 @@ impl TrailingStop { } } -impl PositionStrategy for TrailingStop { +impl PositionStrategy for HiddenTrailingStop { fn name(&self) -> String { - "Trailing stop".into() + "Hidden Trailing Stop".into() } /// Sets the profit state of an open position @@ -149,15 +156,15 @@ impl PositionStrategy for TrailingStop { let pl_perc = position.pl_perc(); let state = { - if pl_perc > TrailingStop::GOOD_PROFIT_PERC { + if pl_perc > HiddenTrailingStop::GOOD_PROFIT_PERC { PositionProfitState::Profit - } else if TrailingStop::MIN_PROFIT_PERC <= pl_perc - && pl_perc < TrailingStop::GOOD_PROFIT_PERC + } else if HiddenTrailingStop::MIN_PROFIT_PERC <= pl_perc + && pl_perc < HiddenTrailingStop::GOOD_PROFIT_PERC { PositionProfitState::MinimumProfit - } else if (0.0..TrailingStop::MIN_PROFIT_PERC).contains(&pl_perc) { + } else if (0.0..HiddenTrailingStop::MIN_PROFIT_PERC).contains(&pl_perc) { PositionProfitState::BreakEven - } else if (TrailingStop::MAX_LOSS_PERC..0.0).contains(&pl_perc) { + } else if (HiddenTrailingStop::MAX_LOSS_PERC..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical @@ -249,21 +256,21 @@ impl PositionStrategy for TrailingStop { } #[derive(Clone, Debug)] -pub struct FastOrderStrategy { +pub struct MarketEnforce { // threshold (%) for which we trigger a market order // to close an open position threshold: f64, } -impl Default for FastOrderStrategy { +impl Default for MarketEnforce { fn default() -> Self { Self { threshold: 0.15 } } } -impl OrderStrategy for FastOrderStrategy { +impl OrderStrategy for MarketEnforce { fn name(&self) -> String { - "Fast order strategy".into() + "Market Enforce".into() } fn on_open_order( @@ -294,7 +301,7 @@ impl OrderStrategy for FastOrderStrategy { messages.push(Message::SubmitOrder { order: OrderForm::new( order.symbol.clone(), - OrderKind::Market {}, + OrderKind::Market, *order.details.platform(), order.details.amount(), ), @@ -303,44 +310,4 @@ impl OrderStrategy for FastOrderStrategy { Ok((None, (!messages.is_empty()).then_some(messages))) } - - // fn on_position_order( - // &self, - // order: &ActiveOrder, - // _: &Position, - // order_book: &OrderBook, - // ) -> Result { - // 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))) - // } } From 762485db3a17c450865cf3656db23c5ff36d9f2c Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 15:57:52 +0000 Subject: [PATCH 14/19] renamed Message into ActionMessage --- rustybot/src/events.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rustybot/src/events.rs b/rustybot/src/events.rs index 1fc24cd..a0f370c 100644 --- a/rustybot/src/events.rs +++ b/rustybot/src/events.rs @@ -5,15 +5,16 @@ use crate::models::OrderForm; #[derive(Debug)] pub struct ActorMessage { - pub(crate) message: Message, + pub(crate) message: ActionMessage, pub(crate) respond_to: oneshot::Sender, } #[derive(Debug)] -pub enum Message { +pub enum ActionMessage { Update { tick: u64 }, ClosePosition { position_id: u64 }, SubmitOrder { order: OrderForm }, + ClosePositionOrders { position_id: u64 }, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] From 53fb8781b3f3fb30aa6d5453cf8615161c8acb1d Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 15:59:32 +0000 Subject: [PATCH 15/19] added trailingstop strategy (useless) but modified hiddentrailing stop: implemented default for both strategies. additionally, the order manager now tries to pair active orders with active positions. order managers now have more APIs such as close orders associated with positions and submit order --- rustybot/src/managers.rs | 169 +++++++++++++++-- rustybot/src/models.rs | 28 +++ rustybot/src/strategy.rs | 389 +++++++++++++++++++++++++++++++++------ 3 files changed, 511 insertions(+), 75 deletions(-) diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index ed32db7..0d828c0 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -12,14 +12,14 @@ use tokio::time::Duration; use crate::connectors::{Client, ExchangeDetails}; use crate::currency::SymbolPair; -use crate::events::{ActorMessage, Event, Message}; +use crate::events::{ActionMessage, ActorMessage, Event}; use crate::models::{ ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, TradingPlatform, }; use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::BoxError; -pub type OptionUpdate = (Option>, Option>); +pub type OptionUpdate = (Option>, Option>); /****************** * PRICES @@ -44,7 +44,7 @@ impl PriceManager { } pub async fn handle_message(&mut self, message: ActorMessage) -> Result<(), BoxError> { - if let Message::Update { tick } = message.message { + if let ActionMessage::Update { tick } = message.message { let a = self.update(tick).await?; self.add_entry(a); } @@ -100,7 +100,7 @@ impl PriceManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -176,7 +176,7 @@ impl PositionManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -221,7 +221,7 @@ impl PositionManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { tick } => self.update(tick).await?, + ActionMessage::Update { tick } => self.update(tick).await?, _ => (None, None), }; @@ -302,7 +302,7 @@ impl PositionManager { ******************/ // Position ID: Order ID -pub type TrackedPositionsMap = HashMap; +pub type TrackedPositionsMap = HashMap>; pub struct OrderManagerHandle { sender: Sender, @@ -344,7 +344,36 @@ impl OrderManagerHandle { self.sender .send(ActorMessage { - message: Message::ClosePosition { position_id }, + message: ActionMessage::ClosePosition { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn close_position_orders( + &mut self, + position_id: u64, + ) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::ClosePositionOrders { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn submit_order(&mut self, order_form: OrderForm) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::SubmitOrder { order: order_form }, respond_to: send, }) .await?; @@ -381,9 +410,14 @@ impl OrderManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { .. } => self.update().await?, - Message::ClosePosition { position_id } => self.close_position(position_id).await?, - _ => (None, None), + ActionMessage::Update { .. } => self.update().await?, + ActionMessage::ClosePosition { position_id } => { + self.close_position(position_id).await? + } + ActionMessage::ClosePositionOrders { position_id } => { + self.close_position_orders(position_id).await? + } + ActionMessage::SubmitOrder { order } => self.submit_order(&order).await?, }; Ok(msg @@ -392,6 +426,53 @@ impl OrderManager { .map_err(|_| BoxError::from("Could not send message."))?) } + pub async fn close_position_orders(&self, position_id: u64) -> Result { + info!("Closing outstanding orders for position #{}", position_id); + + if let Some(position_orders) = self.tracked_positions.get(&position_id) { + // retrieving open orders + let open_orders = self.client.active_orders(&self.pair).await?; + let position_orders: Vec<_> = position_orders + .iter() + .filter_map(|&x| open_orders.iter().find(|y| y.id == x)) + .collect(); + + for order in position_orders { + match self.client.cancel_order(order).await { + Ok(_) => info!("Order #{} closed successfully.", order.id), + Err(e) => error!("Could not close order #{}: {}", order.id, e), + } + } + } + + // TODO: return valid messages and events! + Ok((None, None)) + } + + pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result { + info!("Submiting {}", order_form.kind()); + + let active_order = self.client.submit_order(order_form).await?; + + debug!("Adding order to tracked orders."); + if let Some(metadata) = order_form.metadata() { + if let Some(position_id) = metadata.position_id() { + match self.tracked_positions.get_mut(&position_id) { + None => { + self.tracked_positions + .insert(position_id, vec![active_order.id]); + } + Some(position_orders) => { + position_orders.push(active_order.id); + } + } + } + }; + + // TODO: return valid messages and events!111!!!1! + Ok((None, None)) + } + pub async fn close_position(&mut self, position_id: u64) -> Result { info!("Closing position #{}", position_id); @@ -450,8 +531,8 @@ impl OrderManager { Ok((None, None)) } - pub async fn update(&self) -> Result { - trace!("\t[OrderManager] Updating {}", self.pair); + pub async fn update(&mut self) -> Result { + debug!("\t[OrderManager] Updating {}", self.pair); let (res_open_orders, res_order_book) = tokio::join!( self.client.active_orders(&self.pair), @@ -460,8 +541,49 @@ impl OrderManager { let (open_orders, order_book) = (res_open_orders?, res_order_book?); + // retrieving open positions to check whether the positions have open orders. + // we need to update our internal mapping in that case. + if !open_orders.is_empty() { + let open_positions = self.client.active_positions(&self.pair).await?; + + if let Some(positions) = open_positions { + // currently, we are only trying to match orders with an amount equal to + // a position amount. + for position in positions { + let matching_order = open_orders + .iter() + .find(|x| x.details.amount().abs() == position.amount().abs()); + + // if an order is found, we insert the order to our internal mapping, if not already present + if let Some(matching_order) = matching_order { + match self.tracked_positions.get_mut(&position.id()) { + Some(position_orders) => { + if !position_orders.contains(&matching_order.id) { + trace!( + "Mapped order #{} to position #{}", + position.id(), + matching_order.id + ); + position_orders.push(matching_order.id); + } + } + None => { + trace!( + "Mapped order #{} to position #{}", + position.id(), + matching_order.id + ); + self.tracked_positions + .insert(position.id(), vec![matching_order.id]); + } + } + } + } + } + } + for active_order in open_orders { - debug!( + trace!( "Found open order, calling \"{}\" strategy.", self.strategy.name() ); @@ -471,7 +593,7 @@ impl OrderManager { if let Some(messages) = strat_messages { for m in messages { match m { - Message::SubmitOrder { order: order_form } => { + ActionMessage::SubmitOrder { order: order_form } => { info!("Closing open order..."); info!("\tCancelling open order #{}", &active_order.id); self.client.cancel_order(&active_order).await?; @@ -539,7 +661,7 @@ impl PairManager { position_manager: PositionManagerHandle::new( pair, client, - Box::new(HiddenTrailingStop::new()), + Box::new(HiddenTrailingStop::default()), ), } } @@ -565,8 +687,19 @@ impl PairManager { // TODO: to move into Handler? if let Some(messages) = messages { for m in messages { - if let Message::ClosePosition { position_id } = m { - self.order_manager.close_position(position_id).await?; + match m { + ActionMessage::Update { .. } => {} + ActionMessage::ClosePosition { position_id } => { + self.order_manager.close_position(position_id).await?; + } + ActionMessage::SubmitOrder { order } => { + self.order_manager.submit_order(order).await?; + } + ActionMessage::ClosePositionOrders { position_id } => { + self.order_manager + .close_position_orders(position_id) + .await?; + } } } } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 8873578..01fa2f6 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -265,6 +265,7 @@ pub struct OrderForm { platform: TradingPlatform, amount: f64, leverage: Option, + metadata: Option, } impl OrderForm { @@ -280,6 +281,7 @@ impl OrderForm { platform, amount, leverage: None, + metadata: None, } } @@ -288,6 +290,11 @@ impl OrderForm { self } + pub fn with_metadata(mut self, metadata: OrderMetadata) -> Self { + self.metadata = Some(metadata); + self + } + pub fn pair(&self) -> &SymbolPair { &self.pair } @@ -319,6 +326,27 @@ impl OrderForm { pub fn leverage(&self) -> Option { self.leverage } + + pub fn metadata(&self) -> &Option { + &self.metadata + } +} + +#[derive(Debug, Clone)] +pub struct OrderMetadata { + position_id: Option, +} + +impl OrderMetadata { + pub fn with_position_id(position_id: u64) -> Self { + OrderMetadata { + position_id: Some(position_id), + } + } + + pub fn position_id(&self) -> Option { + self.position_id + } } /*************** diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index e4fe8da..23d4ecd 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -1,13 +1,16 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; + use dyn_clone::DynClone; -use log::info; +use log::{info}; use crate::connectors::Connector; -use crate::events::{Event, EventKind, EventMetadata, Message}; +use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::managers::OptionUpdate; -use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; +use crate::models::{ + ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState, +}; use crate::BoxError; /*************** @@ -21,13 +24,13 @@ pub trait PositionStrategy: DynClone + Send + Sync { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); fn post_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); } impl Debug for dyn PositionStrategy { @@ -69,66 +72,47 @@ impl Debug for dyn OrderStrategy { #[derive(Clone, Debug)] pub struct HiddenTrailingStop { stop_percentages: HashMap, + 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 { - // in percentage - const CAPITAL_MAX_LOSS: f64 = 15.0; - const CAPITAL_MIN_PROFIT: f64 = 9.0; - const CAPITAL_GOOD_PROFIT: f64 = HiddenTrailingStop::CAPITAL_MIN_PROFIT * 2.0; - - // in percentage - const MIN_PROFIT_TRAILING_DELTA: f64 = 0.2; - const GOOD_PROFIT_TRAILING_DELTA: f64 = 0.1; - - const LEVERAGE: f64 = 15.0; - - const MIN_PROFIT_PERC: f64 = (HiddenTrailingStop::CAPITAL_MIN_PROFIT - / HiddenTrailingStop::LEVERAGE) - + HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA; - const GOOD_PROFIT_PERC: f64 = (HiddenTrailingStop::CAPITAL_GOOD_PROFIT - / HiddenTrailingStop::LEVERAGE) - + HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA; - const MAX_LOSS_PERC: f64 = - -(HiddenTrailingStop::CAPITAL_MAX_LOSS / HiddenTrailingStop::LEVERAGE); - - pub fn new() -> Self { - HiddenTrailingStop { - stop_percentages: HashMap::new(), - } - } - 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(HiddenTrailingStop::MIN_PROFIT_TRAILING_DELTA) - } - PositionProfitState::Profit => Some(HiddenTrailingStop::GOOD_PROFIT_TRAILING_DELTA), + 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; - match profit_state { - PositionProfitState::MinimumProfit | PositionProfitState::Profit => { - match self.stop_percentages.get(&position.id()) { - None => { + 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); } - Some(existing_threshold) => { - if existing_threshold < ¤t_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(), @@ -141,6 +125,40 @@ impl HiddenTrailingStop { } } +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() @@ -152,19 +170,17 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>) { + ) -> (Position, Option>, Option>) { let pl_perc = position.pl_perc(); let state = { - if pl_perc > HiddenTrailingStop::GOOD_PROFIT_PERC { + if pl_perc > self.good_profit_percentage { PositionProfitState::Profit - } else if HiddenTrailingStop::MIN_PROFIT_PERC <= pl_perc - && pl_perc < HiddenTrailingStop::GOOD_PROFIT_PERC - { + } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { PositionProfitState::MinimumProfit - } else if (0.0..HiddenTrailingStop::MIN_PROFIT_PERC).contains(&pl_perc) { + } else if (0.0..self.min_profit_percentage).contains(&pl_perc) { PositionProfitState::BreakEven - } else if (HiddenTrailingStop::MAX_LOSS_PERC..0.0).contains(&pl_perc) { + } else if (self.max_loss_percentage..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical @@ -230,8 +246,8 @@ impl PositionStrategy for HiddenTrailingStop { position: Position, _: u64, _: &HashMap, - ) -> (Position, Option>, Option>) { - let close_message = Message::ClosePosition { + ) -> (Position, Option>, Option>) { + let close_message = ActionMessage::ClosePosition { position_id: position.id(), }; @@ -255,6 +271,263 @@ impl PositionStrategy for HiddenTrailingStop { } } +// #[derive(Clone, Debug)] +// pub struct TrailingStop { +// stop_percentages: HashMap, +// 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 { +// 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 < ¤t_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, +// ) -> (Position, Option>, Option>) { +// 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, +// ) -> (Position, Option>, Option>) { +// 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 @@ -264,7 +537,9 @@ pub struct MarketEnforce { impl Default for MarketEnforce { fn default() -> Self { - Self { threshold: 0.15 } + Self { + threshold: 1.0 / 15.0, + } } } @@ -298,7 +573,7 @@ impl OrderStrategy for MarketEnforce { let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; if delta > self.threshold { - messages.push(Message::SubmitOrder { + messages.push(ActionMessage::SubmitOrder { order: OrderForm::new( order.symbol.clone(), OrderKind::Market, From c7c4dd59024b452e22c3f0a50798faf1df0c8b7b Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 16:23:34 +0000 Subject: [PATCH 16/19] changed visibility of ActiveOrder's fields. implemented getters --- rustybot/src/connectors.rs | 53 +++++++++++++-------------- rustybot/src/managers.rs | 28 ++++++++------- rustybot/src/models.rs | 73 +++++++++++++++++++++++++++++++++----- rustybot/src/strategy.rs | 17 ++++----- 4 files changed, 112 insertions(+), 59 deletions(-) diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 596ece5..5b84ee7 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -123,7 +123,7 @@ impl Client { .active_orders(pair) .await? .into_iter() - .filter(|x| &x.symbol == pair) + .filter(|x| &x.pair() == &pair) .collect()) } @@ -350,7 +350,7 @@ impl Connector for BitfinexConnector { // adding leverage, if any match order.leverage() { // TODO: CHANGEME!!!! - Some(leverage) => pre_leverage.with_leverage(15), + Some(_leverage) => pre_leverage.with_leverage(15), // Some(leverage) => pre_leverage.with_leverage(leverage.round() as u32), None => pre_leverage, } @@ -468,7 +468,7 @@ impl Connector for BitfinexConnector { impl From<&ActiveOrder> for CancelOrderForm { fn from(o: &ActiveOrder) -> Self { - Self::from_id(o.id) + Self::from_id(o.id()) } } @@ -476,21 +476,18 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder { type Error = BoxError; fn try_from(response: &OrderResponse) -> Result { - Ok(Self { - exchange: Exchange::Bitfinex, - id: response.id(), - group_id: response.gid(), - client_id: Some(response.cid()), - symbol: SymbolPair::from_str(response.symbol())?, - details: OrderForm::new( - SymbolPair::from_str(response.symbol())?, - response.into(), - response.into(), - response.amount(), - ), - creation_timestamp: 0, - update_timestamp: 0, - }) + let pair = SymbolPair::from_str(response.symbol())?; + + Ok(ActiveOrder::new( + Exchange::Bitfinex, + response.id(), + pair.clone(), + OrderForm::new(pair, response.into(), response.into(), response.amount()), + response.mts_create(), + response.mts_update(), + ) + .with_group_id(response.gid()) + .with_client_id(Some(response.cid()))) } } @@ -670,16 +667,16 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder { fn from(order: &bitfinex::orders::ActiveOrder) -> Self { let pair = SymbolPair::from_str(&order.symbol()).expect("Invalid symbol!"); - Self { - exchange: Exchange::Bitfinex, - id: order.id(), - group_id: order.group_id().map(|x| x as u64), - client_id: Some(order.client_id()), - symbol: pair.clone(), - details: OrderForm::new(pair, order.into(), order.into(), order.amount()), - creation_timestamp: order.creation_timestamp(), - update_timestamp: order.update_timestamp(), - } + ActiveOrder::new( + Exchange::Bitfinex, + order.id(), + pair.clone(), + OrderForm::new(pair, order.into(), order.into(), order.amount()), + order.creation_timestamp(), + order.update_timestamp(), + ) + .with_client_id(Some(order.client_id())) + .with_group_id(order.group_id()) } } diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index 0d828c0..ebc04ba 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -434,13 +434,13 @@ impl OrderManager { let open_orders = self.client.active_orders(&self.pair).await?; let position_orders: Vec<_> = position_orders .iter() - .filter_map(|&x| open_orders.iter().find(|y| y.id == x)) + .filter_map(|&x| open_orders.iter().find(|y| y.id() == x)) .collect(); for order in position_orders { match self.client.cancel_order(order).await { - Ok(_) => info!("Order #{} closed successfully.", order.id), - Err(e) => error!("Could not close order #{}: {}", order.id, e), + Ok(_) => info!("Order #{} closed successfully.", order.id()), + Err(e) => error!("Could not close order #{}: {}", order.id(), e), } } } @@ -460,10 +460,10 @@ impl OrderManager { match self.tracked_positions.get_mut(&position_id) { None => { self.tracked_positions - .insert(position_id, vec![active_order.id]); + .insert(position_id, vec![active_order.id()]); } Some(position_orders) => { - position_orders.push(active_order.id); + position_orders.push(active_order.id()); } } } @@ -493,7 +493,9 @@ impl OrderManager { let opt_position_order = open_orders .iter() // avoid using direct equality, using error margin instead - .find(|x| (x.details.amount().neg() - position.amount()).abs() < 0.0000001); + .find(|x| { + (x.order_form().amount().neg() - position.amount()).abs() < 0.0000001 + }); // checking if the position has an open order. // If so, don't do anything since the order is taken care of @@ -552,29 +554,29 @@ impl OrderManager { for position in positions { let matching_order = open_orders .iter() - .find(|x| x.details.amount().abs() == position.amount().abs()); + .find(|x| x.order_form().amount().abs() == position.amount().abs()); // if an order is found, we insert the order to our internal mapping, if not already present if let Some(matching_order) = matching_order { match self.tracked_positions.get_mut(&position.id()) { Some(position_orders) => { - if !position_orders.contains(&matching_order.id) { + if !position_orders.contains(&matching_order.id()) { trace!( "Mapped order #{} to position #{}", position.id(), - matching_order.id + matching_order.id() ); - position_orders.push(matching_order.id); + position_orders.push(matching_order.id()); } } None => { trace!( "Mapped order #{} to position #{}", position.id(), - matching_order.id + matching_order.id() ); self.tracked_positions - .insert(position.id(), vec![matching_order.id]); + .insert(position.id(), vec![matching_order.id()]); } } } @@ -595,7 +597,7 @@ impl OrderManager { match m { ActionMessage::SubmitOrder { order: order_form } => { info!("Closing open order..."); - info!("\tCancelling open order #{}", &active_order.id); + info!("\tCancelling open order #{}", &active_order.id()); self.client.cancel_order(&active_order).await?; info!("\tSubmitting {}...", order_form.kind()); diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 01fa2f6..9e8cc7c 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -150,14 +150,71 @@ impl OrderDetails { #[derive(Clone, Debug)] pub struct ActiveOrder { - pub(crate) exchange: Exchange, - pub(crate) id: u64, - pub(crate) group_id: Option, - pub(crate) client_id: Option, - pub(crate) symbol: SymbolPair, - pub(crate) details: OrderForm, - pub(crate) creation_timestamp: u64, - pub(crate) update_timestamp: u64, + exchange: Exchange, + id: u64, + group_id: Option, + client_id: Option, + pair: SymbolPair, + order_form: OrderForm, + creation_timestamp: u64, + update_timestamp: u64, +} + +impl ActiveOrder { + pub fn new( + exchange: Exchange, + id: u64, + pair: SymbolPair, + order_form: OrderForm, + creation_timestamp: u64, + update_timestamp: u64, + ) -> Self { + Self { + exchange, + id, + group_id: None, + client_id: None, + pair, + order_form, + creation_timestamp, + update_timestamp, + } + } + + pub fn with_group_id(mut self, group_id: Option) -> Self { + self.group_id = group_id; + self + } + + pub fn with_client_id(mut self, client_id: Option) -> Self { + self.client_id = client_id; + self + } + + pub fn exchange(&self) -> Exchange { + self.exchange + } + pub fn id(&self) -> u64 { + self.id + } + pub fn group_id(&self) -> Option { + self.group_id + } + pub fn client_id(&self) -> Option { + self.client_id + } + pub fn pair(&self) -> &SymbolPair { + &self.pair + } + pub fn order_form(&self) -> &OrderForm { + &self.order_form + } + pub fn creation_timestamp(&self) -> u64 { + self.creation_timestamp + } + pub fn update_timestamp(&self) -> u64 { + self.update_timestamp + } } impl Hash for ActiveOrder { diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 23d4ecd..2503ba0 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -1,16 +1,13 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; - use dyn_clone::DynClone; -use log::{info}; +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::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; use crate::BoxError; /*************** @@ -557,7 +554,7 @@ impl OrderStrategy for MarketEnforce { // long let offer_comparison = { - if order.details.amount() > 0.0 { + if order.order_form().amount() > 0.0 { order_book.highest_bid() } else { order_book.lowest_ask() @@ -567,7 +564,7 @@ impl OrderStrategy for MarketEnforce { // if the best offer is higher than our threshold, // ask the manager to close the position with a market order let order_price = order - .details + .order_form() .price() .ok_or("The active order does not have a price!")?; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; @@ -575,10 +572,10 @@ impl OrderStrategy for MarketEnforce { if delta > self.threshold { messages.push(ActionMessage::SubmitOrder { order: OrderForm::new( - order.symbol.clone(), + order.pair().clone(), OrderKind::Market, - *order.details.platform(), - order.details.amount(), + *order.order_form().platform(), + order.order_form().amount(), ), }) } From 848043d7581b92c284f59a11e5e7dfc08e53af5b Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 16:31:47 +0000 Subject: [PATCH 17/19] changed signature of with_... functions to get Option --- rustybot/src/models.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 9e8cc7c..2eae4bd 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -342,13 +342,13 @@ impl OrderForm { } } - pub fn with_leverage(mut self, leverage: f64) -> Self { - self.leverage = Some(leverage); + pub fn with_leverage(mut self, leverage: Option) -> Self { + self.leverage = leverage; self } - pub fn with_metadata(mut self, metadata: OrderMetadata) -> Self { - self.metadata = Some(metadata); + pub fn with_metadata(mut self, metadata: Option) -> Self { + self.metadata = metadata; self } From 881defa081e9b151e3d0b8a861132dc8dbdfa8de Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 16:32:08 +0000 Subject: [PATCH 18/19] enriched orderform in EnforceMarket --- rustybot/src/strategy.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 2503ba0..1052c57 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -576,7 +576,9 @@ impl OrderStrategy for MarketEnforce { OrderKind::Market, *order.order_form().platform(), order.order_form().amount(), - ), + ) + .with_leverage(order.order_form().leverage()) + .with_metadata(order.order_form().metadata().clone()), }) } From 2d3d1ca69cf6fbe9a25a60c6c43d947382942806 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Wed, 17 Feb 2021 16:32:47 +0000 Subject: [PATCH 19/19] using right platform when sending Limit order --- rustybot/src/managers.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index ebc04ba..e2ceee2 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -14,7 +14,7 @@ use crate::connectors::{Client, ExchangeDetails}; use crate::currency::SymbolPair; use crate::events::{ActionMessage, ActorMessage, Event}; use crate::models::{ - ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, TradingPlatform, + ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, }; use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::BoxError; @@ -505,16 +505,15 @@ impl OrderManager { // No open order, undercutting best price with limit order let closing_price = self.best_closing_price(&position, &order_book); - // TODO: hardcoded platform to Derivative! let order_form = OrderForm::new( self.pair.clone(), OrderKind::Limit { price: closing_price, }, - TradingPlatform::Derivative, + position.platform(), position.amount().neg(), ) - .with_leverage(position.leverage()); + .with_leverage(Some(position.leverage())); info!("Submitting {} order", order_form.kind()); if let Err(e) = self.client.submit_order(&order_form).await {