Compare commits

...

45 Commits
rust ... master

Author SHA1 Message Date
Giulio De Pasquale
d08e175ea7 Time to change BfxBot to Rustico 2021-03-02 17:17:13 +00:00
Giulio De Pasquale
bf85e39c2c Passing allllllllll the currencies boi 2021-03-02 17:16:38 +00:00
Giulio De Pasquale
fbe445f712 Bot now takes a vec of pairs directly 2021-03-02 17:16:27 +00:00
Giulio De Pasquale
78b5b4d0f7 Inverted quote and base in SymbolPair constructor 2021-03-02 17:16:06 +00:00
Giulio De Pasquale
4f7d9042d6 added polka dot deriv 2021-03-02 16:45:54 +00:00
Giulio De Pasquale
f43fcb0206 First steps towards REST + WS connectors (BFX lib still in the works) 2021-02-27 20:49:56 +00:00
Giulio De Pasquale
354ef407f3 added ada deriv 2021-02-27 19:37:28 +00:00
Giulio De Pasquale
a3ce3682ad clippy + reformat 2021-02-26 16:23:50 +00:00
Giulio De Pasquale
f4431f26c0 imported profitstate 2021-02-26 16:16:51 +00:00
Giulio De Pasquale
545bfe28de reformat code 2021-02-26 16:15:07 +00:00
Giulio De Pasquale
8bd9eb048d play min profit and good profit sounds when coming from break even as well 2021-02-26 16:11:54 +00:00
Giulio De Pasquale
093269b173 removed extra spaces 2021-02-26 16:11:34 +00:00
Giulio De Pasquale
5cd6f8d37c Added sounds on market order placement and profit state changes 2021-02-26 16:02:55 +00:00
Giulio De Pasquale
a2cddead14 added super mario sounds, no code 2021-02-26 15:10:32 +00:00
Giulio De Pasquale
8e6e58435d Added cfg(test) to common mod in tests 2021-02-25 20:41:51 +00:00
Giulio De Pasquale
df45866b5f Added todo 2021-02-25 20:39:04 +00:00
Giulio De Pasquale
793fd40b4c Print when in Critical/Loss as well 2021-02-25 20:19:07 +00:00
Giulio De Pasquale
888843d853 Added dummy initial tests 2021-02-25 20:15:56 +00:00
Giulio De Pasquale
9be8c2e6ff cleanup of TrailingStop update_stop_percentage helper function 2021-02-25 19:38:39 +00:00
Giulio De Pasquale
1948b0f032 Update stop percentage and print before checking for existing stop percentage 2021-02-25 19:30:06 +00:00
Giulio De Pasquale
fdc93e9cbe Comments 2021-02-25 19:25:23 +00:00
Giulio De Pasquale
d8d0e07a0b Renamed HiddenTrailingStop to TrailingStop (again!?) 2021-02-25 19:22:19 +00:00
Giulio De Pasquale
415944bde5 Added is_long(), is_short() functions 2021-02-25 19:21:50 +00:00
Giulio De Pasquale
6d873f3c14 Log to file. Added date to log.
Changed bin name to rustico.
2021-02-25 12:52:52 +00:00
Giulio De Pasquale
f31c778d66 Updated OrderForm construction for Bitfinex following updates on lib 2021-02-24 11:09:56 +00:00
Giulio De Pasquale
5e39f2767f default max_loss relative to min_profit 2021-02-22 11:27:25 +00:00
Giulio De Pasquale
4df9e38569 added slippage when submitting stop order 2021-02-22 10:03:48 +00:00
Giulio De Pasquale
0df1511a59 set default leverage to 15, again 2021-02-22 09:31:39 +00:00
Giulio De Pasquale
47ddc44721 removed unused fields, imports and functions 2021-02-22 00:30:19 +00:00
Giulio De Pasquale
f4d7786e03 revamped order tracking in order manager 2021-02-22 00:27:26 +00:00
Giulio De Pasquale
551ca054c6 extracted status printing method in HiddenTrailingStop strategy.
added leverage to stop order in HiddenTrailingStop strategy.
2021-02-22 00:25:21 +00:00
Giulio De Pasquale
a73e59e0e3 hardcoded leverage to Bitfinex connector response 2021-02-22 00:24:27 +00:00
Giulio De Pasquale
20b838f635 hardcoded leverage to Bitfinex connector response 2021-02-22 00:23:40 +00:00
Giulio De Pasquale
2aa9bcdb95 removed commented trailingstop strategy 2021-02-21 18:41:40 +00:00
Giulio De Pasquale
3adceef1e9 changed log messaged 2021-02-21 18:38:20 +00:00
Giulio De Pasquale
4592d755aa using order manager order API to submit limit orders. added position id to metadata 2021-02-20 23:25:54 +00:00
Giulio De Pasquale
ae54166ea2 removed leverage debug msg 2021-02-20 22:23:03 +00:00
Giulio De Pasquale
b5007c8f95 set leverage to 15 2021-02-20 22:19:00 +00:00
Giulio De Pasquale
634f86c6fa orders are now per orderform and not per order manager. current order strategy is set only when executing limit order on profit by trailing stop 2021-02-20 22:17:53 +00:00
Giulio De Pasquale
669bd70946 added todo 2021-02-20 22:16:57 +00:00
Giulio De Pasquale
2d81358fa0 implemented stop order for trailing stop. position strategy now receives fees information. added fees API to client. 2021-02-20 21:01:32 +00:00
Giulio De Pasquale
386137f16e fixed Display for Stop order. changed field to stop_price for StopLimit 2021-02-20 21:00:50 +00:00
Giulio De Pasquale
35d967e6d9 modified event constructor, added builder method with_metadata.
code cleanup
2021-02-20 19:27:15 +00:00
Giulio De Pasquale
7252dd4f8b print errors when setting up env and logger 2021-02-19 17:28:21 +00:00
23e5f2fbae Merge pull request 'web sources removed' (#12) from rust into master
Reviewed-on: https://giugl.io/gitea/peperunas/rustico/pulls/12
2021-02-18 10:46:25 +01:00
17 changed files with 786 additions and 626 deletions

73
Cargo.lock generated
View File

@ -2,9 +2,9 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.14.0" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
@ -75,9 +75,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.55" version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -109,7 +109,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"tokio", "tokio",
"tungstenite", "tungstenite 0.13.0",
"url", "url",
] ]
@ -237,6 +237,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
[[package]]
name = "ears"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e741c53078ec208f07c837fca26cd2624f959140c52268ee0677ad3b8e9b2112"
dependencies = [
"lazy_static",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.26" version = "0.8.26"
@ -791,9 +802,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.22.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
@ -1159,7 +1170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[package]] [[package]]
name = "rustybot" name = "rustico"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
@ -1168,6 +1179,7 @@ dependencies = [
"chrono", "chrono",
"dotenv", "dotenv",
"dyn-clone", "dyn-clone",
"ears",
"fern", "fern",
"float-cmp", "float-cmp",
"futures-retry", "futures-retry",
@ -1177,7 +1189,7 @@ dependencies = [
"regex", "regex",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"tungstenite", "tungstenite 0.12.0",
] ]
[[package]] [[package]]
@ -1341,6 +1353,26 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "thiserror"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.0.1" version = "1.0.1"
@ -1438,7 +1470,7 @@ dependencies = [
"log 0.4.11", "log 0.4.11",
"pin-project 1.0.2", "pin-project 1.0.2",
"tokio", "tokio",
"tungstenite", "tungstenite 0.12.0",
] ]
[[package]] [[package]]
@ -1519,6 +1551,27 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "tungstenite"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093"
dependencies = [
"base64",
"byteorder",
"bytes 1.0.1",
"http",
"httparse",
"input_buffer",
"log 0.4.11",
"native-tls",
"rand 0.8.2",
"sha-1",
"thiserror",
"url",
"utf-8",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.12.0" version = "1.12.0"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "rustybot" name = "rustico"
version = "0.1.0" version = "0.1.0"
authors = ["Giulio De Pasquale <depasquale@giugl.io>"] authors = ["Giulio De Pasquale <depasquale@giugl.io>"]
edition = "2018" edition = "2018"
@ -23,3 +23,4 @@ futures-retry = "0.6"
tungstenite = "0.12" tungstenite = "0.12"
tokio-tungstenite = "0.13" tokio-tungstenite = "0.13"
dotenv = "0.15" dotenv = "0.15"
ears = "0.8.0"

BIN
sounds/smas-smb3_goal.wav Normal file

Binary file not shown.

BIN
sounds/smw2_boing.wav Normal file

Binary file not shown.

BIN
sounds/smw_1-up.wav Normal file

Binary file not shown.

BIN
sounds/smw_power-up.wav Normal file

Binary file not shown.

View File

@ -3,37 +3,32 @@ use core::time::Duration;
use log::{error, info}; use log::{error, info};
use tokio::time::sleep; use tokio::time::sleep;
use crate::BoxError;
use crate::connectors::ExchangeDetails; use crate::connectors::ExchangeDetails;
use crate::currency::{Symbol, SymbolPair}; use crate::currency::{Symbol, SymbolPair};
use crate::frontend::FrontendManagerHandle; use crate::frontend::FrontendManagerHandle;
use crate::managers::ExchangeManager; use crate::managers::ExchangeManager;
use crate::ticker::Ticker; use crate::ticker::Ticker;
use crate::BoxError;
pub struct BfxBot { pub struct Rustico {
ticker: Ticker, ticker: Ticker,
exchange_managers: Vec<ExchangeManager>, exchange_managers: Vec<ExchangeManager>,
frontend_connector: FrontendManagerHandle, frontend_connector: FrontendManagerHandle,
} }
impl BfxBot { impl Rustico {
// TODO: change constructor to take SymbolPairs and not Symbol
pub fn new( pub fn new(
exchanges: Vec<ExchangeDetails>, exchanges: Vec<ExchangeDetails>,
trading_symbols: Vec<Symbol>, trading_pairs: Vec<SymbolPair>,
quote: Symbol,
tick_duration: Duration, tick_duration: Duration,
) -> Self { ) -> Self {
let pairs: Vec<_> = trading_symbols
.iter()
.map(|x| SymbolPair::new(quote.clone(), x.clone()))
.collect();
let exchange_managers = exchanges let exchange_managers = exchanges
.iter() .iter()
.map(|x| ExchangeManager::new(x, &pairs)) .map(|x| ExchangeManager::new(x, &trading_pairs))
.collect(); .collect();
BfxBot { Rustico {
ticker: Ticker::new(tick_duration), ticker: Ticker::new(tick_duration),
exchange_managers, exchange_managers,
frontend_connector: FrontendManagerHandle::new(), frontend_connector: FrontendManagerHandle::new(),

View File

@ -4,22 +4,23 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bitfinex::api::Bitfinex; use bitfinex::api::RestClient;
use bitfinex::book::BookPrecision; use bitfinex::book::BookPrecision;
use bitfinex::orders::{CancelOrderForm, OrderMeta}; use bitfinex::orders::{CancelOrderForm, OrderMeta};
use bitfinex::responses::{OrderResponse, TradeResponse}; use bitfinex::responses::{OrderResponse, TradeResponse};
use bitfinex::ticker::TradingPairTicker; use bitfinex::ticker::TradingPairTicker;
use bitfinex::websockets::WebSocketClient;
use futures_retry::RetryPolicy; use futures_retry::RetryPolicy;
use log::trace; use log::trace;
use tokio::macros::support::Future; use tokio::macros::support::Future;
use tokio::time::Duration; use tokio::time::Duration;
use crate::BoxError;
use crate::currency::{Symbol, SymbolPair}; use crate::currency::{Symbol, SymbolPair};
use crate::models::{ use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind, PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
}; };
use crate::BoxError;
#[derive(PartialEq, Eq, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Exchange { pub enum Exchange {
@ -36,7 +37,7 @@ pub enum ExchangeDetails {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Client { pub struct Client {
exchange: Exchange, exchange: Exchange,
inner: Arc<Box<dyn Connector>>, inner: Arc<Box<dyn RestConnector>>,
} }
impl Client { impl Client {
@ -123,7 +124,7 @@ impl Client {
.active_orders(pair) .active_orders(pair)
.await? .await?
.into_iter() .into_iter()
.filter(|x| &x.pair() == &pair) .filter(|x| x.pair() == pair)
.collect()) .collect())
} }
@ -164,10 +165,13 @@ impl Client {
) -> Result<Option<Vec<OrderDetails>>, BoxError> { ) -> Result<Option<Vec<OrderDetails>>, BoxError> {
self.inner.orders_history(pair).await self.inner.orders_history(pair).await
} }
pub async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { self.inner.trading_fees().await }
} }
/// This trait represents a REST API service.
#[async_trait] #[async_trait]
pub trait Connector: Send + Sync { pub trait RestConnector: Send + Sync {
fn name(&self) -> String; fn name(&self) -> String;
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError>; async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError>;
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError>; async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError>;
@ -191,18 +195,26 @@ pub trait Connector: Send + Sync {
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>; async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>;
} }
impl Debug for dyn Connector { impl Debug for dyn RestConnector {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.name()) write!(f, "{}", self.name())
} }
} }
/// This trait represents a WebSocket API service.
#[async_trait]
pub trait WebSocketConnector: Send + Sync {
fn name(&self) -> String;
async fn connect(&self) -> Result<(), BoxError>;
}
/************** /**************
* BITFINEX * BITFINEX
**************/ **************/
pub struct BitfinexConnector { pub struct BitfinexConnector {
bfx: Bitfinex, rest: bitfinex::api::RestClient,
ws: bitfinex::websockets::WebSocketClient,
} }
impl BitfinexConnector { impl BitfinexConnector {
@ -217,7 +229,8 @@ impl BitfinexConnector {
pub fn new(api_key: &str, api_secret: &str) -> Self { pub fn new(api_key: &str, api_secret: &str) -> Self {
BitfinexConnector { BitfinexConnector {
bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())), rest: RestClient::new(Some(api_key.into()), Some(api_secret.into())),
ws: WebSocketClient::new(),
} }
} }
@ -237,7 +250,7 @@ impl BitfinexConnector {
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError> async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
where where
F: FnMut() -> Fut, F: FnMut() -> Fut,
Fut: Future<Output = Result<O, BoxError>>, Fut: Future<Output=Result<O, BoxError>>,
{ {
let response = { let response = {
loop { loop {
@ -258,14 +271,14 @@ impl BitfinexConnector {
} }
#[async_trait] #[async_trait]
impl Connector for BitfinexConnector { impl RestConnector for BitfinexConnector {
fn name(&self) -> String { fn name(&self) -> String {
"Bitfinex".into() "Bitfinex REST".into()
} }
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> { async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
let active_positions = let active_positions =
BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?; BitfinexConnector::retry_nonce(|| self.rest.positions.active_positions()).await?;
let positions: Vec<_> = active_positions let positions: Vec<_> = active_positions
.into_iter() .into_iter()
@ -273,14 +286,14 @@ impl Connector for BitfinexConnector {
.filter(|x: &Position| x.pair() == pair) .filter(|x: &Position| x.pair() == pair)
.collect(); .collect();
trace!("\t[PositionManager] Retrieved positions for {}", pair); trace!("\tRetrieved positions for {}", pair);
Ok((!positions.is_empty()).then_some(positions)) Ok((!positions.is_empty()).then_some(positions))
} }
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> { async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
let symbol_name = BitfinexConnector::format_trading_pair(pair); let symbol_name = BitfinexConnector::format_trading_pair(pair);
let ticker: TradingPairTicker = self.bfx.ticker.trading_pair(symbol_name).await?; let ticker: TradingPairTicker = self.rest.ticker.trading_pair(symbol_name).await?;
Ok(ticker) Ok(ticker)
} }
@ -289,7 +302,7 @@ impl Connector for BitfinexConnector {
let symbol_name = BitfinexConnector::format_trading_pair(pair); let symbol_name = BitfinexConnector::format_trading_pair(pair);
let response = BitfinexConnector::retry_nonce(|| { let response = BitfinexConnector::retry_nonce(|| {
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0) self.rest.book.trading_pair(&symbol_name, BookPrecision::P0)
}) })
.await?; .await?;
@ -306,7 +319,7 @@ impl Connector for BitfinexConnector {
} }
async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> { async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?; let response = BitfinexConnector::retry_nonce(|| self.rest.orders.active_orders()).await?;
Ok(response.iter().map(Into::into).collect()) Ok(response.iter().map(Into::into).collect())
} }
@ -316,7 +329,6 @@ impl Connector for BitfinexConnector {
let amount = order.amount(); let amount = order.amount();
let order_form = { let order_form = {
let pre_leverage = {
match order.kind() { match order.kind() {
OrderKind::Limit { price } => { OrderKind::Limit { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
@ -327,13 +339,13 @@ impl Connector for BitfinexConnector {
OrderKind::Stop { price } => { OrderKind::Stop { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
} }
OrderKind::StopLimit { price, limit_price } => { OrderKind::StopLimit { stop_price: price, limit_price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
.with_price_aux_limit(limit_price)? .with_price_aux_limit(Some(limit_price))?
} }
OrderKind::TrailingStop { distance } => { OrderKind::TrailingStop { distance } => {
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
.with_price_trailing(distance)? .with_price_trailing(Some(distance))?
} }
OrderKind::FillOrKill { price } => { OrderKind::FillOrKill { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
@ -342,31 +354,28 @@ impl Connector for BitfinexConnector {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
} }
} }
.with_meta(OrderMeta::new( .with_meta(Some(OrderMeta::new(
BitfinexConnector::AFFILIATE_CODE.to_string(), BitfinexConnector::AFFILIATE_CODE.to_string(),
)) )))
}; // TODO: CHANGEME!
.with_leverage(Some(15))
// adding leverage, if any
match order.leverage() {
// TODO: CHANGEME!!!!
Some(_leverage) => pre_leverage.with_leverage(15),
// Some(leverage) => pre_leverage.with_leverage(leverage.round() as u32),
None => pre_leverage,
}
}; };
let response = let response =
BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?; BitfinexConnector::retry_nonce(|| self.rest.orders.submit_order(&order_form)).await?;
Ok((&response).try_into()?) // parsing response into ActiveOrder and adding leverage from order form
let order_response: ActiveOrder = (&response).try_into()?;
// TODO: CHANGEME!!!!
Ok(order_response.with_leverage(Some(15.0)))
} }
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> { async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
let cancel_form = order.into(); let cancel_form = order.into();
let response = let response =
BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?; BitfinexConnector::retry_nonce(|| self.rest.orders.cancel_order(&cancel_form)).await?;
Ok((&response).try_into()?) Ok((&response).try_into()?)
} }
@ -379,7 +388,7 @@ impl Connector for BitfinexConnector {
amount: f64, amount: f64,
) -> Result<(), BoxError> { ) -> Result<(), BoxError> {
BitfinexConnector::retry_nonce(|| { BitfinexConnector::retry_nonce(|| {
self.bfx.account.transfer_between_wallets( self.rest.account.transfer_between_wallets(
from.into(), from.into(),
to.into(), to.into(),
symbol.to_string(), symbol.to_string(),
@ -396,7 +405,7 @@ impl Connector for BitfinexConnector {
order: &OrderDetails, order: &OrderDetails,
) -> Result<Option<Vec<Trade>>, BoxError> { ) -> Result<Option<Vec<Trade>>, BoxError> {
let response = BitfinexConnector::retry_nonce(|| { let response = BitfinexConnector::retry_nonce(|| {
self.bfx self.rest
.trades .trades
.generated_by_order(order.pair().trading_repr(), order.id()) .generated_by_order(order.pair().trading_repr(), order.id())
}) })
@ -414,7 +423,7 @@ impl Connector for BitfinexConnector {
pair: &SymbolPair, pair: &SymbolPair,
) -> Result<Option<Vec<OrderDetails>>, BoxError> { ) -> Result<Option<Vec<OrderDetails>>, BoxError> {
let response = let response =
BitfinexConnector::retry_nonce(|| self.bfx.orders.history(Some(pair.trading_repr()))) BitfinexConnector::retry_nonce(|| self.rest.orders.history(Some(pair.trading_repr())))
.await?; .await?;
let mapped_vec: Vec<_> = response.iter().map(Into::into).collect(); let mapped_vec: Vec<_> = response.iter().map(Into::into).collect();
@ -424,7 +433,7 @@ impl Connector for BitfinexConnector {
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> {
let mut fees = vec![]; let mut fees = vec![];
let accountfees = let accountfees =
BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?; BitfinexConnector::retry_nonce(|| self.rest.account.account_summary()).await?;
// Derivatives // Derivatives
let derivative_taker = TradingFees::Taker { let derivative_taker = TradingFees::Taker {
@ -466,6 +475,17 @@ impl Connector for BitfinexConnector {
} }
} }
#[async_trait]
impl WebSocketConnector for BitfinexConnector {
fn name(&self) -> String {
"Bitfinex WS".into()
}
async fn connect(&self) -> Result<(), BoxError> {
Ok(())
}
}
impl From<&ActiveOrder> for CancelOrderForm { impl From<&ActiveOrder> for CancelOrderForm {
fn from(o: &ActiveOrder) -> Self { fn from(o: &ActiveOrder) -> Self {
Self::from_id(o.id()) Self::from_id(o.id())
@ -511,7 +531,6 @@ impl TryInto<Position> for bitfinex::positions::Position {
} }
}; };
println!("leverage: {}", self.leverage());
Ok(Position::new( Ok(Position::new(
SymbolPair::from_str(self.symbol())?, SymbolPair::from_str(self.symbol())?,
state, state,
@ -603,7 +622,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(), stop_price: response.price(),
limit_price: response.price_aux_limit().expect("Limit price not found!"), limit_price: response.price_aux_limit().expect("Limit price not found!"),
}, },
bitfinex::orders::OrderKind::TrailingStop bitfinex::orders::OrderKind::TrailingStop
@ -642,7 +661,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(), stop_price: response.price(),
limit_price: response.price_aux_limit().expect("Limit price not found!"), limit_price: response.price_aux_limit().expect("Limit price not found!"),
}, },
bitfinex::orders::OrderKind::TrailingStop bitfinex::orders::OrderKind::TrailingStop

View File

@ -13,7 +13,7 @@ pub struct Symbol {
} }
impl<S> From<S> for Symbol impl<S> From<S> for Symbol
where where
S: Into<String>, S: Into<String>,
{ {
fn from(item: S) -> Self { fn from(item: S) -> Self {
@ -31,6 +31,8 @@ impl Symbol {
pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0"); pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0");
pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0"); pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0");
pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0"); pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0");
pub const DERIV_ADA: Symbol = Symbol::new_static("ADAF0");
pub const DERIV_POLKADOT: Symbol = Symbol::new_static("DOTF0");
// Paper trading // Paper trading
pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC");
@ -74,8 +76,8 @@ pub struct SymbolPair {
} }
impl SymbolPair { impl SymbolPair {
pub fn new(quote: Symbol, base: Symbol) -> Self { pub fn new(base: Symbol, quote: Symbol) -> Self {
SymbolPair { quote, base } SymbolPair { base, quote }
} }
pub fn trading_repr(&self) -> String { pub fn trading_repr(&self) -> String {
format!("t{}{}", self.base, self.quote) format!("t{}{}", self.base, self.quote)

View File

@ -56,14 +56,19 @@ pub struct Event {
} }
impl Event { impl Event {
pub fn new(kind: EventKind, tick: u64, metadata: Option<EventMetadata>) -> Self { pub fn new(kind: EventKind, tick: u64) -> Self {
Event { Event {
kind, kind,
tick, tick,
metadata, metadata: None,
} }
} }
pub fn with_metadata(mut self, metadata: Option<EventMetadata>) -> Self {
self.metadata = metadata;
self
}
fn has_metadata(&self) -> bool { fn has_metadata(&self) -> bool {
self.metadata.is_some() self.metadata.is_some()
} }

View File

@ -1,16 +1,15 @@
use log::info;
use tokio::sync::mpsc::{channel, Receiver, Sender};
use crate::events::{ActorMessage};
use crate::BoxError;
use futures_util::stream::TryStreamExt;
use futures_util::StreamExt;
use std::net::SocketAddr; use std::net::SocketAddr;
use futures_util::stream::TryStreamExt;
use futures_util::StreamExt;
use log::info;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio_tungstenite::accept_async; use tokio_tungstenite::accept_async;
use crate::BoxError;
use crate::events::ActorMessage;
#[derive(Debug)] #[derive(Debug)]
pub struct FrontendManager { pub struct FrontendManager {
receiver: Receiver<ActorMessage>, receiver: Receiver<ActorMessage>,

View File

@ -4,12 +4,13 @@
use std::env; use std::env;
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter::{Trace}; use log::error;
use log::LevelFilter::Info;
use tokio::time::Duration; use tokio::time::Duration;
use crate::bot::BfxBot; use crate::bot::Rustico;
use crate::connectors::ExchangeDetails; use crate::connectors::ExchangeDetails;
use crate::currency::Symbol; use crate::currency::{Symbol, SymbolPair};
mod bot; mod bot;
mod connectors; mod connectors;
@ -20,13 +21,22 @@ mod managers;
mod models; mod models;
mod strategy; mod strategy;
mod ticker; mod ticker;
mod tests;
mod sounds;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>; pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), BoxError> { async fn main() -> Result<(), BoxError> {
setup_logger()?; if let Err(e) = setup_logger() {
dotenv::dotenv()?; error!("Could not setup logger: {}", e);
return Err(e.into());
}
if let Err(e) = dotenv::dotenv() {
error!("Could not open .env file: {}", e);
return Err(e.into());
}
let api_key = env::vars() let api_key = env::vars()
.find(|(k, _v)| k == "API_KEY") .find(|(k, _v)| k == "API_KEY")
@ -38,14 +48,24 @@ async fn main() -> Result<(), BoxError> {
.ok_or("API_SECRET not set!")?; .ok_or("API_SECRET not set!")?;
let bitfinex = ExchangeDetails::Bitfinex { let bitfinex = ExchangeDetails::Bitfinex {
api_key: api_key.into(), api_key,
api_secret: api_secret.into(), api_secret,
}; };
let mut bot = BfxBot::new( let pairs = vec![
SymbolPair::new(Symbol::BTC, Symbol::USD),
SymbolPair::new(Symbol::XMR, Symbol::USD),
SymbolPair::new(Symbol::ETH, Symbol::USD),
SymbolPair::new(Symbol::DERIV_ADA, Symbol::DERIV_USDT),
SymbolPair::new(Symbol::DERIV_POLKADOT, Symbol::DERIV_USDT),
SymbolPair::new(Symbol::DERIV_BTC, Symbol::DERIV_USDT),
SymbolPair::new(Symbol::DERIV_ETH, Symbol::DERIV_USDT),
SymbolPair::new(Symbol::DERIV_TESTBTC, Symbol::DERIV_TESTUSDT),
];
let mut bot = Rustico::new(
vec![bitfinex], vec![bitfinex],
vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC], pairs,
Symbol::DERIV_USDT,
Duration::new(10, 0), Duration::new(10, 0),
); );
@ -63,16 +83,17 @@ fn setup_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new() fern::Dispatch::new()
.format(move |out, message, record| { .format(move |out, message, record| {
out.finish(format_args!( out.finish(format_args!(
"[{}][{}] {}", "{} | [{}][{}] | {}",
record.target(), chrono::Local::now().format("[%d/%m/%Y][%H:%M:%S]"),
record.target().strip_prefix("rustico::").unwrap_or("rustico"),
colors.color(record.level()), colors.color(record.level()),
message message
)) ))
}) })
.level(Trace) .level(Info)
.filter(|metadata| metadata.target().contains("rustybot")) .filter(|metadata| metadata.target().contains("rustico"))
.chain(std::io::stdout()) .chain(std::io::stdout())
// .chain(fern::log_file("rustico.log")?) .chain(fern::log_file("rustico.log")?)
.apply()?; .apply()?;
Ok(()) Ok(())

View File

@ -1,23 +1,22 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::ops::Neg; use std::ops::Neg;
use futures_util::stream::FuturesUnordered; use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
use merge::Merge; use merge::Merge;
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::mpsc::channel;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::time::Duration; use tokio::time::Duration;
use crate::BoxError;
use crate::connectors::{Client, ExchangeDetails}; use crate::connectors::{Client, ExchangeDetails};
use crate::currency::SymbolPair; use crate::currency::SymbolPair;
use crate::events::{ActionMessage, ActorMessage, Event}; use crate::events::{ActionMessage, ActorMessage, Event};
use crate::models::{ use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PriceTicker};
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, use crate::sounds::{MARKET_ORDER_PLACED_PATH, play_sound};
}; use crate::strategy::{MarketEnforce, PositionStrategy, TrailingStop};
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
use crate::BoxError;
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>); pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
@ -234,8 +233,9 @@ impl PositionManager {
pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> { pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> {
trace!("\t[PositionManager] Updating {}", self.pair); trace!("\t[PositionManager] Updating {}", self.pair);
let opt_active_positions = self.client.active_positions(&self.pair).await?;
self.current_tick = tick; self.current_tick = tick;
let (fees, opt_active_positions) = tokio::join!(self.client.trading_fees(),self.client.active_positions(&self.pair));
let (fees, opt_active_positions) = (fees?, opt_active_positions?);
// we assume there is only ONE active position per pair // we assume there is only ONE active position per pair
match opt_active_positions { match opt_active_positions {
@ -258,11 +258,11 @@ impl PositionManager {
let (pos_on_tick, events_on_tick, messages_on_tick) = self let (pos_on_tick, events_on_tick, messages_on_tick) = self
.strategy .strategy
.on_tick(position, self.current_tick(), &self.positions_history); .on_tick(position, self.current_tick(), &self.positions_history, &fees);
let (pos_post_tick, events_post_tick, messages_post_tick) = self let (pos_post_tick, events_post_tick, messages_post_tick) = self
.strategy .strategy
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history); .post_tick(pos_on_tick, self.current_tick(), &self.positions_history, &fees);
events.merge(events_on_tick); events.merge(events_on_tick);
events.merge(events_post_tick); events.merge(events_post_tick);
@ -280,30 +280,12 @@ impl PositionManager {
} }
}; };
} }
pub fn position_previous_tick(&self, id: u64, tick: Option<u64>) -> Option<&Position> {
let tick = match tick {
Some(tick) => {
if tick < 1 {
1
} else {
tick
}
}
None => self.current_tick() - 1,
};
self.positions_history.get(&tick).filter(|x| x.id() == id)
}
} }
/****************** /******************
* ORDERS * ORDERS
******************/ ******************/
// Position ID: Order ID
pub type TrackedPositionsMap = HashMap<u64, Vec<u64>>;
pub struct OrderManagerHandle { pub struct OrderManagerHandle {
sender: Sender<ActorMessage>, sender: Sender<ActorMessage>,
} }
@ -329,10 +311,10 @@ impl OrderManagerHandle {
} }
} }
pub fn new(pair: SymbolPair, client: Client, strategy: Box<dyn OrderStrategy>) -> Self { pub fn new(pair: SymbolPair, client: Client) -> Self {
let (sender, receiver) = channel(1); let (sender, receiver) = channel(1);
let manager = OrderManager::new(receiver, pair, client, strategy); let manager = OrderManager::new(receiver, pair, client);
tokio::spawn(OrderManagerHandle::run_order_manager(manager)); tokio::spawn(OrderManagerHandle::run_order_manager(manager));
@ -384,11 +366,9 @@ impl OrderManagerHandle {
pub struct OrderManager { pub struct OrderManager {
receiver: Receiver<ActorMessage>, receiver: Receiver<ActorMessage>,
tracked_positions: TrackedPositionsMap, orders_map: HashMap<u64, HashSet<ActiveOrder>>,
pair: SymbolPair, pair: SymbolPair,
open_orders: Vec<ActiveOrder>,
client: Client, client: Client,
strategy: Box<dyn OrderStrategy>,
} }
impl OrderManager { impl OrderManager {
@ -396,18 +376,93 @@ impl OrderManager {
receiver: Receiver<ActorMessage>, receiver: Receiver<ActorMessage>,
pair: SymbolPair, pair: SymbolPair,
client: Client, client: Client,
strategy: Box<dyn OrderStrategy>,
) -> Self { ) -> Self {
OrderManager { OrderManager {
receiver, receiver,
pair, pair,
open_orders: Vec::new(),
client, client,
strategy, orders_map: Default::default(),
tracked_positions: HashMap::new(),
} }
} }
/*
* PRIVATE METHODS
*/
fn add_to_orders_map(&mut self, position_id: u64, order: ActiveOrder) -> bool {
self.orders_map
.entry(position_id)
.or_default()
.insert(order)
}
fn orders_from_position_id(&self, position_id: u64) -> Option<&HashSet<ActiveOrder>> {
self.orders_map.get(&position_id)
}
fn all_tracked_orders(&self) -> Option<Vec<ActiveOrder>> {
let orders: Vec<_> = self.orders_map.values().flat_map(|x| x.clone()).collect();
(!orders.is_empty()).then_some(orders)
}
async fn update_orders_map_from_remote(&mut self) -> Result<(), BoxError> {
let (res_remote_orders, res_remote_positions) = tokio::join!(self.client.active_orders(&self.pair),
self.client.active_positions(&self.pair));
let (remote_orders, remote_positions) = (res_remote_orders?, res_remote_positions?);
match remote_positions {
// no positions open, clear internal mapping
None => { self.orders_map.clear(); }
Some(positions) => {
// retain only positions that are open remotely as well
self.orders_map.retain(|local_id, _| positions.iter().any(|r| r.id() == *local_id));
for position in positions {
// mapping tracked orders to their ids
let tracked_orders: Vec<_> = self.orders_from_position_id(position.id())
.iter()
.flat_map(|x| x
.iter()
.map(|x| x.id()))
.collect();
// adding remote order that are not in the internal mapping
for remote_order in remote_orders.iter().filter(|x| !tracked_orders.contains(&x.id())) {
// the only check to bind an active order to an open position,
// is to check for their amount which should be identical
if (remote_order.order_form().amount().abs() - position.amount().abs()).abs() < 0.0001 {
trace!("Adding order {} to internal mapping from remote.", remote_order.id());
self.add_to_orders_map(position.id(), remote_order.clone());
}
}
// removing local orders that are not in remote
for local_orders in self.orders_map.values_mut() {
local_orders.retain(|l| remote_orders.iter().any(|r| r.id() == l.id()));
}
// clean-up empty positions in local mapping
let empty_positions_id: Vec<_> = self.orders_map
.iter()
.filter(|(_, orders)| orders.is_empty())
.map(|(&position, _)| position)
.collect();
for position_id in empty_positions_id {
self.orders_map.remove(&position_id);
}
}
}
}
Ok(())
}
/*
* PUBLIC METHODS
*/
pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> {
let (events, messages) = match msg.message { let (events, messages) = match msg.message {
ActionMessage::Update { .. } => self.update().await?, ActionMessage::Update { .. } => self.update().await?,
@ -429,14 +484,7 @@ impl OrderManager {
pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> { pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> {
info!("Closing outstanding orders for position #{}", position_id); info!("Closing outstanding orders for position #{}", position_id);
if let Some(position_orders) = self.tracked_positions.get(&position_id) { if let Some(position_orders) = self.orders_map.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 { for order in position_orders {
match self.client.cancel_order(order).await { match self.client.cancel_order(order).await {
Ok(_) => info!("Order #{} closed successfully.", order.id()), Ok(_) => info!("Order #{} closed successfully.", order.id()),
@ -450,25 +498,33 @@ impl OrderManager {
} }
pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result<OptionUpdate, BoxError> { pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result<OptionUpdate, BoxError> {
info!("Submiting {}", order_form.kind()); info!("Submitting order: {}", order_form.kind());
let active_order = self.client.submit_order(order_form).await?; // adding strategy to order, if present in the metadata
let active_order = {
debug!("Adding order to tracked orders.");
if let Some(metadata) = order_form.metadata() { if let Some(metadata) = order_form.metadata() {
if let Some(position_id) = metadata.position_id() { // TODO: this seems extremely dirty. Double check!
match self.tracked_positions.get_mut(&position_id) { self.client.submit_order(order_form).await?.with_strategy(metadata.cloned_strategy())
None => { } else {
self.tracked_positions self.client.submit_order(order_form).await?
.insert(position_id, vec![active_order.id()]);
}
Some(position_orders) => {
position_orders.push(active_order.id());
}
}
} }
}; };
if let Some(metadata) = order_form.metadata() {
if let Some(position_id) = metadata.position_id() {
debug!("Adding order to tracked orders.");
if !self.add_to_orders_map(position_id, active_order) {
error!("Failed while adding order to internal mapping.");
};
}
};
// play sound if Market order is placed
if let OrderKind::Market = order_form.kind() {
play_sound(MARKET_ORDER_PLACED_PATH);
}
// TODO: return valid messages and events!111!!!1! // TODO: return valid messages and events!111!!!1!
Ok((None, None)) Ok((None, None))
} }
@ -513,10 +569,14 @@ impl OrderManager {
position.platform(), position.platform(),
position.amount().neg(), position.amount().neg(),
) )
.with_leverage(Some(position.leverage())); .with_leverage(Some(position.leverage()))
.with_metadata(Some(OrderMetadata::new()
.with_strategy(Some(Box::new(MarketEnforce::default())))
.with_position_id(Some(position.id())))
);
info!("Submitting {} order", order_form.kind()); // submitting order
if let Err(e) = self.client.submit_order(&order_form).await { if let Err(e) = self.submit_order(&order_form).await {
error!( error!(
"Could not submit {} to close position #{}: {}", "Could not submit {} to close position #{}: {}",
order_form.kind(), order_form.kind(),
@ -535,61 +595,26 @@ impl OrderManager {
pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> { pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
debug!("\t[OrderManager] Updating {}", self.pair); debug!("\t[OrderManager] Updating {}", self.pair);
let (res_open_orders, res_order_book) = tokio::join!( // updating internal orders' mapping from remote
self.client.active_orders(&self.pair), self.update_orders_map_from_remote().await?;
self.client.order_book(&self.pair)
);
let (open_orders, order_book) = (res_open_orders?, res_order_book?); // calling strategies for the orders and collecting resulting messages
let _orders_messages: HashMap<&ActiveOrder, Vec<ActionMessage>> = HashMap::new();
// retrieving open positions to check whether the positions have open orders. if let Some(tracked_orders) = self.all_tracked_orders() {
// we need to update our internal mapping in that case. // since there are open orders, retrieve order book
if !open_orders.is_empty() { let order_book = self.client.order_book(&self.pair).await?;
let open_positions = self.client.active_positions(&self.pair).await?;
if let Some(positions) = open_positions { for active_order in tracked_orders.iter().filter(|x| x.strategy().is_some()) {
// currently, we are only trying to match orders with an amount equal to let strategy = active_order.strategy().as_ref().unwrap();
// a position amount.
for position in positions {
let matching_order = open_orders
.iter()
.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()) {
trace!( trace!(
"Mapped order #{} to position #{}", "Found open order with \"{}\" strategy.",
position.id(), strategy.name()
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 {
trace!(
"Found open order, calling \"{}\" strategy.",
self.strategy.name()
); );
let (_, strat_messages) = self.strategy.on_open_order(&active_order, &order_book)?; // executing the order's strategy and collecting its messages, if any
let (_, strat_messages) = strategy.on_open_order(&active_order, &order_book)?;
if let Some(messages) = strat_messages { if let Some(messages) = strat_messages {
for m in messages { for m in messages {
@ -600,7 +625,7 @@ impl OrderManager {
self.client.cancel_order(&active_order).await?; self.client.cancel_order(&active_order).await?;
info!("\tSubmitting {}...", order_form.kind()); info!("\tSubmitting {}...", order_form.kind());
self.client.submit_order(&order_form).await?; self.submit_order(&order_form).await?;
info!("Done!"); info!("Done!");
} }
_ => { _ => {
@ -612,10 +637,12 @@ impl OrderManager {
} }
} }
} }
}
Ok((None, None)) Ok((None, None))
} }
pub fn best_closing_price(&self, position: &Position, order_book: &OrderBook) -> f64 { pub fn best_closing_price(&self, position: &Position, order_book: &OrderBook) -> f64 {
let ask = order_book.lowest_ask(); let ask = order_book.lowest_ask();
let bid = order_book.highest_bid(); let bid = order_book.highest_bid();
@ -657,12 +684,11 @@ impl PairManager {
order_manager: OrderManagerHandle::new( order_manager: OrderManagerHandle::new(
pair.clone(), pair.clone(),
client.clone(), client.clone(),
Box::new(MarketEnforce::default()),
), ),
position_manager: PositionManagerHandle::new( position_manager: PositionManagerHandle::new(
pair, pair,
client, client,
Box::new(HiddenTrailingStop::default()), Box::new(TrailingStop::default()),
), ),
} }
} }

View File

@ -2,8 +2,11 @@ use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use dyn_clone::clone_box;
use crate::connectors::Exchange; use crate::connectors::Exchange;
use crate::currency::{Symbol, SymbolPair}; use crate::currency::{Symbol, SymbolPair};
use crate::strategy::OrderStrategy;
/*************** /***************
* Prices * Prices
@ -148,7 +151,7 @@ impl OrderDetails {
} }
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub struct ActiveOrder { pub struct ActiveOrder {
exchange: Exchange, exchange: Exchange,
id: u64, id: u64,
@ -158,6 +161,7 @@ pub struct ActiveOrder {
order_form: OrderForm, order_form: OrderForm,
creation_timestamp: u64, creation_timestamp: u64,
update_timestamp: u64, update_timestamp: u64,
strategy: Option<Box<dyn OrderStrategy>>,
} }
impl ActiveOrder { impl ActiveOrder {
@ -178,6 +182,7 @@ impl ActiveOrder {
order_form, order_form,
creation_timestamp, creation_timestamp,
update_timestamp, update_timestamp,
strategy: None,
} }
} }
@ -191,6 +196,16 @@ impl ActiveOrder {
self self
} }
pub fn with_strategy(mut self, strategy: Option<Box<dyn OrderStrategy>>) -> Self {
self.strategy = strategy;
self
}
pub fn with_leverage(mut self, leverage: Option<f64>) -> Self {
self.order_form = self.order_form.with_leverage(leverage);
self
}
pub fn exchange(&self) -> Exchange { pub fn exchange(&self) -> Exchange {
self.exchange self.exchange
} }
@ -215,6 +230,9 @@ impl ActiveOrder {
pub fn update_timestamp(&self) -> u64 { pub fn update_timestamp(&self) -> u64 {
self.update_timestamp self.update_timestamp
} }
pub fn strategy(&self) -> &Option<Box<dyn OrderStrategy>> {
&self.strategy
}
} }
impl Hash for ActiveOrder { impl Hash for ActiveOrder {
@ -231,6 +249,22 @@ impl PartialEq for ActiveOrder {
impl Eq for ActiveOrder {} impl Eq for ActiveOrder {}
impl Clone for ActiveOrder {
fn clone(&self) -> Self {
Self {
exchange: self.exchange,
id: self.id,
group_id: self.group_id,
client_id: self.client_id,
pair: self.pair.clone(),
order_form: self.order_form.clone(),
creation_timestamp: self.creation_timestamp,
update_timestamp: self.update_timestamp,
strategy: self.strategy.as_ref().map(|x| clone_box(&**x)),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum TradingPlatform { pub enum TradingPlatform {
Exchange, Exchange,
@ -261,7 +295,7 @@ pub enum OrderKind {
Limit { price: f64 }, Limit { price: f64 },
Market, Market,
Stop { price: f64 }, Stop { price: f64 },
StopLimit { price: f64, limit_price: f64 }, StopLimit { stop_price: f64, limit_price: f64 },
TrailingStop { distance: f64 }, TrailingStop { distance: f64 },
FillOrKill { price: f64 }, FillOrKill { price: f64 },
ImmediateOrCancel { price: f64 }, ImmediateOrCancel { price: f64 },
@ -285,31 +319,31 @@ impl Display for OrderKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
OrderKind::Limit { price } => { OrderKind::Limit { price } => {
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
} }
OrderKind::Market => { OrderKind::Market => {
write!(f, "[{}]", self.as_str()) write!(f, "[{}]", self.as_str())
} }
OrderKind::Stop { price } => { OrderKind::Stop { price } => {
write!(f, "[{} | Price: {:0.5}", self.as_str(), price,) write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
} }
OrderKind::StopLimit { price, limit_price } => { OrderKind::StopLimit { stop_price, limit_price } => {
write!( write!(
f, f,
"[{} | Price: {:0.5}, Limit Price: {:0.5}]", "[{} | Stop: {:0.5}, Limit: {:0.5}]",
self.as_str(), self.as_str(),
price, stop_price,
limit_price limit_price
) )
} }
OrderKind::TrailingStop { distance } => { OrderKind::TrailingStop { distance } => {
write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,) write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance, )
} }
OrderKind::FillOrKill { price } => { OrderKind::FillOrKill { price } => {
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
} }
OrderKind::ImmediateOrCancel { price } => { OrderKind::ImmediateOrCancel { price } => {
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
} }
} }
} }
@ -355,55 +389,90 @@ impl OrderForm {
pub fn pair(&self) -> &SymbolPair { pub fn pair(&self) -> &SymbolPair {
&self.pair &self.pair
} }
pub fn kind(&self) -> OrderKind { pub fn kind(&self) -> OrderKind {
self.kind self.kind
} }
pub fn platform(&self) -> &TradingPlatform { pub fn platform(&self) -> &TradingPlatform {
&self.platform &self.platform
} }
pub fn amount(&self) -> f64 { pub fn amount(&self) -> f64 {
self.amount self.amount
} }
pub fn price(&self) -> Option<f64> { pub fn price(&self) -> Option<f64> {
match self.kind { match self.kind {
OrderKind::Limit { price, .. } => Some(price), OrderKind::Limit { price, .. } => Some(price),
OrderKind::Market { .. } => None, OrderKind::Market { .. } => None,
OrderKind::Stop { price, .. } => Some(price), OrderKind::Stop { price, .. } => Some(price),
OrderKind::StopLimit { price, .. } => Some(price), OrderKind::StopLimit { stop_price: price, .. } => Some(price),
OrderKind::TrailingStop { .. } => None, OrderKind::TrailingStop { .. } => None,
OrderKind::FillOrKill { price, .. } => Some(price), OrderKind::FillOrKill { price, .. } => Some(price),
OrderKind::ImmediateOrCancel { price, .. } => Some(price), OrderKind::ImmediateOrCancel { price, .. } => Some(price),
} }
} }
pub fn leverage(&self) -> Option<f64> { pub fn leverage(&self) -> Option<f64> {
self.leverage self.leverage
} }
pub fn metadata(&self) -> &Option<OrderMetadata> { pub fn metadata(&self) -> &Option<OrderMetadata> {
&self.metadata &self.metadata
} }
pub fn is_long(&self) -> bool {
self.amount.is_sign_positive()
}
pub fn is_short(&self) -> bool {
self.amount.is_sign_negative()
}
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct OrderMetadata { pub struct OrderMetadata {
position_id: Option<u64>, position_id: Option<u64>,
strategy: Option<Box<dyn OrderStrategy>>,
}
impl Clone for OrderMetadata {
fn clone(&self) -> Self {
Self {
position_id: self.position_id,
strategy: self.strategy.as_ref().map(|x| clone_box(&**x)),
}
}
} }
impl OrderMetadata { impl OrderMetadata {
pub fn with_position_id(position_id: u64) -> Self { pub fn new() -> Self {
OrderMetadata { Self {
position_id: Some(position_id), position_id: None,
strategy: None,
} }
} }
pub fn with_position_id(mut self, position_id: Option<u64>) -> Self {
self.position_id = position_id;
self
}
pub fn with_strategy(mut self, strategy: Option<Box<dyn OrderStrategy>>) -> Self {
self.strategy = strategy;
self
}
pub fn position_id(&self) -> Option<u64> { pub fn position_id(&self) -> Option<u64> {
self.position_id self.position_id
} }
pub fn cloned_strategy(&self) -> Option<Box<dyn OrderStrategy>> {
match &self.strategy {
None => { None }
Some(strategy) => {
Some(clone_box(&**strategy))
}
}
}
}
impl Default for OrderMetadata {
fn default() -> Self {
Self::new()
}
} }
/*************** /***************

19
src/sounds.rs Normal file
View File

@ -0,0 +1,19 @@
use ears::{AudioController, Sound};
use log::error;
pub const MARKET_ORDER_PLACED_PATH: &str = "sounds/smas-smb3_goal.wav";
pub const LOSS_TO_BREAK_EVEN_PATH: &str = "sounds/smw2_boing.wav";
pub const MIN_PROFIT_SOUND_PATH: &str = "sounds/smw_1-up.wav";
pub const GOOD_PROFIT_SOUND_PATH: &str = "sounds/smw_power-up.wav";
pub fn play_sound(sound_path: &'static str) {
std::thread::spawn(move || {
match Sound::new(sound_path) {
Ok(mut sound) => {
sound.play();
while sound.is_playing() {}
}
Err(e) => { error!("Could not play {}: {}", sound_path, e); }
}
});
}

View File

@ -1,14 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::ops::Neg;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use log::info; use log::info;
use crate::connectors::Connector; use crate::BoxError;
use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
use crate::managers::OptionUpdate; use crate::managers::OptionUpdate;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees};
use crate::BoxError; use crate::models::PositionProfitState::{BreakEven, Critical, Loss, MinimumProfit, Profit};
use crate::sounds::{GOOD_PROFIT_SOUND_PATH, LOSS_TO_BREAK_EVEN_PATH, MIN_PROFIT_SOUND_PATH, play_sound};
/*************** /***************
* DEFINITIONS * DEFINITIONS
@ -21,12 +23,14 @@ pub trait PositionStrategy: DynClone + Send + Sync {
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>); ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
fn post_tick( fn post_tick(
&mut self, &mut self,
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>); ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
} }
@ -67,8 +71,16 @@ impl Debug for dyn OrderStrategy {
***************/ ***************/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HiddenTrailingStop { pub struct TrailingStop {
// Position ID: stop percentage mapping
stop_percentages: HashMap<u64, f64>, stop_percentages: HashMap<u64, f64>,
// Position ID: bool mapping. Represents when the strategy has asked the
// order manager to set a stop loss order
stop_loss_flags: HashMap<u64, bool>,
// Position ID: bool mapping. Represents when the strategy has asked the
// order manager to set a limit order to close the position as the stop percentage
// has been surpassed
trail_set_flags: HashMap<u64, bool>,
capital_max_loss: f64, capital_max_loss: f64,
capital_min_profit: f64, capital_min_profit: f64,
capital_good_profit: f64, capital_good_profit: f64,
@ -80,55 +92,96 @@ pub struct HiddenTrailingStop {
max_loss_percentage: f64, max_loss_percentage: f64,
} }
impl HiddenTrailingStop {
fn update_stop_percentage(&mut self, position: &Position) {
if let Some(profit_state) = position.profit_state() {
let profit_state_delta = match profit_state {
PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta),
PositionProfitState::Profit => Some(self.good_profit_trailing_delta),
_ => None,
};
if let Some(profit_state_delta) = profit_state_delta { impl TrailingStop {
let current_stop_percentage = position.pl_perc() - profit_state_delta; fn play_sound_on_state(prev_position: &Position, current_position: &Position) {
if prev_position.profit_state().is_none() {
return;
}
if let PositionProfitState::MinimumProfit | PositionProfitState::Profit = if current_position.profit_state().is_none() {
profit_state return;
{ }
let prev_state = prev_position.profit_state().unwrap();
let current_state = current_position.profit_state().unwrap();
// negative to positive
if let Loss | Critical = prev_state {
match current_state {
PositionProfitState::BreakEven => { play_sound(LOSS_TO_BREAK_EVEN_PATH); }
PositionProfitState::MinimumProfit => { play_sound(MIN_PROFIT_SOUND_PATH); }
PositionProfitState::Profit => { play_sound(GOOD_PROFIT_SOUND_PATH); }
_ => {}
}
}
if let BreakEven = prev_state {
match current_state {
PositionProfitState::MinimumProfit => { play_sound(MIN_PROFIT_SOUND_PATH); }
PositionProfitState::Profit => { play_sound(GOOD_PROFIT_SOUND_PATH); }
_ => {}
}
}
}
fn print_status(&self, position: &Position) {
match self.stop_percentages.get(&position.id()) { match self.stop_percentages.get(&position.id()) {
None => { None => {
self.stop_percentages info!(
.insert(position.id(), current_stop_percentage); "\tState: {:?} | PL: {:0.2}{} ({:0.2}%)",
position.profit_state().unwrap(),
position.pl(),
position.pair().quote(),
position.pl_perc()
);
} }
Some(existing_threshold) => { Some(stop_percentage) => {
if existing_threshold < &current_stop_percentage {
self.stop_percentages
.insert(position.id(), current_stop_percentage);
}
}
}
}
}
info!( info!(
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
position.profit_state().unwrap(), position.profit_state().unwrap(),
position.pl(), position.pl(),
position.pair().quote(), position.pair().quote(),
position.pl_perc(), position.pl_perc(),
self.stop_percentages.get(&position.id()).unwrap_or(&0.0) stop_percentage
); );
} }
} }
}
fn update_stop_percentage(&mut self, position: &Position) {
if let Some(profit_state) = position.profit_state() {
let profit_state_delta = match profit_state {
PositionProfitState::MinimumProfit => self.min_profit_trailing_delta,
PositionProfitState::Profit => self.good_profit_trailing_delta,
_ => return
};
let current_trailing_delta = position.pl_perc() - profit_state_delta;
match self.stop_percentages.get(&position.id()) {
None => {
self.stop_percentages
.insert(position.id(), current_trailing_delta);
}
Some(existing_threshold) => {
if existing_threshold < &current_trailing_delta {
self.stop_percentages
.insert(position.id(), current_trailing_delta);
}
}
}
}
}
} }
impl Default for HiddenTrailingStop { impl Default for TrailingStop {
fn default() -> Self { fn default() -> Self {
let leverage = 5.0; let leverage = 15.0;
// in percentage // in percentage
let capital_max_loss = 15.0; let capital_min_profit = 8.5;
let capital_min_profit = 9.0; let capital_max_loss = capital_min_profit * 1.9;
let capital_good_profit = capital_min_profit * 2.0; let capital_good_profit = capital_min_profit * 2.0;
let weighted_min_profit = capital_min_profit / leverage; let weighted_min_profit = capital_min_profit / leverage;
@ -142,8 +195,10 @@ impl Default for HiddenTrailingStop {
let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta; let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
let max_loss_percentage = -weighted_max_loss; let max_loss_percentage = -weighted_max_loss;
HiddenTrailingStop { TrailingStop {
stop_percentages: Default::default(), stop_percentages: Default::default(),
stop_loss_flags: Default::default(),
trail_set_flags: Default::default(),
capital_max_loss, capital_max_loss,
capital_min_profit, capital_min_profit,
capital_good_profit, capital_good_profit,
@ -156,9 +211,10 @@ impl Default for HiddenTrailingStop {
} }
} }
} }
impl PositionStrategy for HiddenTrailingStop {
impl PositionStrategy for TrailingStop {
fn name(&self) -> String { fn name(&self) -> String {
"Hidden Trailing Stop".into() "Trailing Stop".into()
} }
/// Sets the profit state of an open position /// Sets the profit state of an open position
@ -167,20 +223,22 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
_: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let pl_perc = position.pl_perc(); let pl_perc = position.pl_perc();
// setting the state of the position based on its profit/loss percentage
let state = { let state = {
if pl_perc > self.good_profit_percentage { if pl_perc > self.good_profit_percentage {
PositionProfitState::Profit Profit
} else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
PositionProfitState::MinimumProfit MinimumProfit
} else if (0.0..self.min_profit_percentage).contains(&pl_perc) { } else if (0.0..self.min_profit_percentage).contains(&pl_perc) {
PositionProfitState::BreakEven BreakEven
} else if (self.max_loss_percentage..0.0).contains(&pl_perc) { } else if (self.max_loss_percentage..0.0).contains(&pl_perc) {
PositionProfitState::Loss Loss
} else { } else {
PositionProfitState::Critical Critical
} }
}; };
@ -188,54 +246,53 @@ impl PositionStrategy for HiddenTrailingStop {
let event_metadata = EventMetadata::new(Some(position.id()), None); let event_metadata = EventMetadata::new(Some(position.id()), None);
let new_position = position.with_profit_state(Some(state)); let new_position = position.with_profit_state(Some(state));
// checking if there was a state change between the current position
// and its last state
match opt_prev_position { match opt_prev_position {
Some(prev) => { Some(prev) => {
if prev.profit_state() == Some(state) { if prev.profit_state() == Some(state) {
return (new_position, None, None); return (new_position, None, None);
} }
TrailingStop::play_sound_on_state(&prev, &new_position);
} }
None => return (new_position, None, None), None => return (new_position, None, None),
}; };
let events = { let event = match state {
let mut events = vec![]; PositionProfitState::Critical => {
Event::new(
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, EventKind::ReachedMaxLoss,
current_tick, current_tick,
Some(event_metadata), )
));
} }
PositionProfitState::Loss => {
Event::new(
EventKind::ReachedLoss,
current_tick,
)
}
PositionProfitState::BreakEven => {
Event::new(
EventKind::ReachedBreakEven,
current_tick,
)
}
PositionProfitState::MinimumProfit => {
Event::new(
EventKind::ReachedMinProfit,
current_tick,
)
}
PositionProfitState::Profit => {
Event::new(
EventKind::ReachedGoodProfit,
current_tick,
)
}
}.with_metadata(Some(event_metadata));
events (new_position, Some(vec![event]), None)
};
(new_position, Some(events), None)
} }
fn post_tick( fn post_tick(
@ -243,284 +300,89 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position, position: Position,
_: u64, _: u64,
_: &HashMap<u64, Position>, _: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let close_message = ActionMessage::ClosePosition { let taker_fee = fees
position_id: position.id(), .iter()
.filter_map(|x| match x {
TradingFees::Taker {
platform,
percentage,
} if platform == &position.platform() => Some(percentage),
_ => None,
})
.next().map_or_else(|| 0.0, |&x| x);
// we need to consider possible slippage when executing the stop order
let slippage_percentage = self.max_loss_percentage * 0.085;
// calculating the stop price based on short/long position
let stop_loss_price = {
if position.is_short() {
position.base_price() * (1.0 - (self.max_loss_percentage - taker_fee - slippage_percentage) / 100.0)
} else {
position.base_price() * (1.0 + (self.max_loss_percentage - taker_fee - slippage_percentage) / 100.0)
}
}; };
// if critical, early return with close position let close_position_orders_msg = ActionMessage::ClosePositionOrders {
if let Some(PositionProfitState::Critical) = position.profit_state() { position_id: position.id(),
info!("Maximum loss reached. Closing position.");
return (position, None, Some(vec![close_message]));
}; };
let close_position_msg = ActionMessage::ClosePosition {
position_id: position.id(),
};
let set_stop_loss_msg = ActionMessage::SubmitOrder {
order: OrderForm::new(position.pair().clone(),
OrderKind::Stop { price: stop_loss_price },
position.platform(),
position.amount().neg())
.with_leverage(Some(self.leverage))
.with_metadata(Some(OrderMetadata::new().with_position_id(Some(position.id()))))
};
let stop_loss_set = *self.stop_loss_flags.entry(position.id()).or_insert(false);
// if in loss, ask the order manager to set the stop limit order,
// if not already set
if let Some(PositionProfitState::Critical) | Some(PositionProfitState::Loss) = position.profit_state() {
self.print_status(&position);
if !stop_loss_set {
info!("In loss. Opening trailing stop order.");
self.stop_loss_flags.insert(position.id(), true);
return (position, None, Some(vec![set_stop_loss_msg]));
}
return (position, None, None);
}
// if we get here we are with a profit/loss ration > 0.0
let mut messages = vec![];
// if a stop loss order was previously set,
// ask the order manager to remove the order first
if stop_loss_set {
info!("Removing stop loss order.");
messages.push(close_position_orders_msg);
self.stop_loss_flags.insert(position.id(), false);
}
self.update_stop_percentage(&position);
self.print_status(&position);
// let's check if we surpassed an existing stop percentage // let's check if we surpassed an existing stop percentage
if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) { if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
if &position.pl_perc() <= existing_stop_percentage { if &position.pl_perc() <= existing_stop_percentage {
info!("Stop percentage surpassed. Closing position."); info!("Stop percentage surpassed. Closing position.");
return (position, None, Some(vec![close_message])); messages.push(close_position_msg);
return (position, None, Some(messages));
} }
} }
self.update_stop_percentage(&position); (position, None, Some(messages))
(position, None, None)
} }
} }
// #[derive(Clone, Debug)]
// pub struct TrailingStop {
// stop_percentages: HashMap<u64, f64>,
// 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<OrderForm> {
// 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 < &current_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<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// 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<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// 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 * ORDER STRATEGIES
*/ */
@ -535,7 +397,7 @@ pub struct MarketEnforce {
impl Default for MarketEnforce { impl Default for MarketEnforce {
fn default() -> Self { fn default() -> Self {
Self { Self {
threshold: 1.0 / 15.0, threshold: 1.2 / 15.0,
} }
} }
} }
@ -554,7 +416,7 @@ impl OrderStrategy for MarketEnforce {
// long // long
let offer_comparison = { let offer_comparison = {
if order.order_form().amount() > 0.0 { if order.order_form().is_long() {
order_book.highest_bid() order_book.highest_bid()
} else { } else {
order_book.lowest_ask() order_book.lowest_ask()

89
src/tests.rs Normal file
View File

@ -0,0 +1,89 @@
#[cfg(test)]
mod common {
use crate::currency::{Symbol, SymbolPair};
use crate::models::{Position, PositionState, TradingPlatform};
use crate::models::PositionProfitState::Loss;
// TODO: generate other helper generator functions like the one below
// Generates two short positions with different profit/loss ratios. Both are position in "Loss".
pub fn get_short_loss_positions(pair: SymbolPair) -> (Position, Position) {
let almost_critical = Position::new(pair.clone(),
PositionState::Open,
-0.1,
100.0,
-2.0,
-2.0,
150.0,
0,
TradingPlatform::Margin,
0.0)
.with_profit_state(Some(Loss));
let loss = Position::new(pair.clone(),
PositionState::Open,
-0.1,
100.0,
-1.0,
-1.0,
150.0,
0,
TradingPlatform::Margin,
0.0)
.with_profit_state(Some(Loss));
(almost_critical, loss)
}
pub fn get_btcusd_pair() -> SymbolPair {
SymbolPair::new(Symbol::BTC, Symbol::USD)
}
}
#[cfg(test)]
mod positions {
use crate::models::{Position, PositionState, TradingPlatform};
use crate::models::PositionProfitState::Loss;
use crate::tests::common::{get_btcusd_pair, get_short_loss_positions};
#[test]
fn short_positions() {
let pair = get_btcusd_pair();
let one = Position::new(pair.clone(),
PositionState::Open,
-0.1,
100.0,
-2.0,
-2.0,
150.0,
0,
TradingPlatform::Margin,
0.0);
assert_eq!(one.pair(), &pair);
assert_eq!(one.is_long(), false);
assert_eq!(one.is_short(), true);
assert_eq!(one.profit_state(), None);
assert_eq!(one.platform(), TradingPlatform::Margin);
assert_eq!(one.amount(), -0.1);
assert_eq!(one.base_price(), 100.0);
assert_eq!(one.pl(), -2.0);
assert_eq!(one.pl_perc(), -2.0);
assert_eq!(one.id(), 0);
assert_eq!(one.leverage(), 0.0);
assert_eq!(one.price_liq(), 150.0);
assert_eq!(one.state(), PositionState::Open);
assert!(one.price_liq() > one.base_price());
let (two, three) = get_short_loss_positions(pair);
assert_eq!(two.is_short(), true);
assert_eq!(two.is_long(), false);
assert_eq!(three.is_short(), true);
assert_eq!(three.is_long(), false);
assert_eq!(two.profit_state(), Some(Loss));
assert_eq!(three.profit_state(), Some(Loss));
// TODO: add more test positions with and without profit states
}
}