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.
[[package]]
name = "addr2line"
version = "0.14.0"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
dependencies = [
"gimli",
]
@ -75,9 +75,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.55"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598"
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
dependencies = [
"addr2line",
"cfg-if 1.0.0",
@ -109,7 +109,7 @@ dependencies = [
"serde_derive",
"serde_json",
"tokio",
"tungstenite",
"tungstenite 0.13.0",
"url",
]
@ -237,6 +237,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "encoding_rs"
version = "0.8.26"
@ -791,9 +802,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
[[package]]
name = "once_cell"
@ -1159,7 +1170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[package]]
name = "rustybot"
name = "rustico"
version = "0.1.0"
dependencies = [
"async-trait",
@ -1168,6 +1179,7 @@ dependencies = [
"chrono",
"dotenv",
"dyn-clone",
"ears",
"fern",
"float-cmp",
"futures-retry",
@ -1177,7 +1189,7 @@ dependencies = [
"regex",
"tokio",
"tokio-tungstenite",
"tungstenite",
"tungstenite 0.12.0",
]
[[package]]
@ -1341,6 +1353,26 @@ dependencies = [
"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]]
name = "thread_local"
version = "1.0.1"
@ -1438,7 +1470,7 @@ dependencies = [
"log 0.4.11",
"pin-project 1.0.2",
"tokio",
"tungstenite",
"tungstenite 0.12.0",
]
[[package]]
@ -1519,6 +1551,27 @@ dependencies = [
"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]]
name = "typenum"
version = "1.12.0"

View File

@ -1,5 +1,5 @@
[package]
name = "rustybot"
name = "rustico"
version = "0.1.0"
authors = ["Giulio De Pasquale <depasquale@giugl.io>"]
edition = "2018"
@ -23,3 +23,4 @@ futures-retry = "0.6"
tungstenite = "0.12"
tokio-tungstenite = "0.13"
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 tokio::time::sleep;
use crate::BoxError;
use crate::connectors::ExchangeDetails;
use crate::currency::{Symbol, SymbolPair};
use crate::frontend::FrontendManagerHandle;
use crate::managers::ExchangeManager;
use crate::ticker::Ticker;
use crate::BoxError;
pub struct BfxBot {
pub struct Rustico {
ticker: Ticker,
exchange_managers: Vec<ExchangeManager>,
frontend_connector: FrontendManagerHandle,
}
impl BfxBot {
impl Rustico {
// TODO: change constructor to take SymbolPairs and not Symbol
pub fn new(
exchanges: Vec<ExchangeDetails>,
trading_symbols: Vec<Symbol>,
quote: Symbol,
trading_pairs: Vec<SymbolPair>,
tick_duration: Duration,
) -> Self {
let pairs: Vec<_> = trading_symbols
.iter()
.map(|x| SymbolPair::new(quote.clone(), x.clone()))
.collect();
let exchange_managers = exchanges
.iter()
.map(|x| ExchangeManager::new(x, &pairs))
.map(|x| ExchangeManager::new(x, &trading_pairs))
.collect();
BfxBot {
Rustico {
ticker: Ticker::new(tick_duration),
exchange_managers,
frontend_connector: FrontendManagerHandle::new(),

View File

@ -4,22 +4,23 @@ use std::str::FromStr;
use std::sync::Arc;
use async_trait::async_trait;
use bitfinex::api::Bitfinex;
use bitfinex::api::RestClient;
use bitfinex::book::BookPrecision;
use bitfinex::orders::{CancelOrderForm, OrderMeta};
use bitfinex::responses::{OrderResponse, TradeResponse};
use bitfinex::ticker::TradingPairTicker;
use bitfinex::websockets::WebSocketClient;
use futures_retry::RetryPolicy;
use log::trace;
use tokio::macros::support::Future;
use tokio::time::Duration;
use crate::BoxError;
use crate::currency::{Symbol, SymbolPair};
use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
};
use crate::BoxError;
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Exchange {
@ -36,7 +37,7 @@ pub enum ExchangeDetails {
#[derive(Clone, Debug)]
pub struct Client {
exchange: Exchange,
inner: Arc<Box<dyn Connector>>,
inner: Arc<Box<dyn RestConnector>>,
}
impl Client {
@ -123,7 +124,7 @@ impl Client {
.active_orders(pair)
.await?
.into_iter()
.filter(|x| &x.pair() == &pair)
.filter(|x| x.pair() == pair)
.collect())
}
@ -164,10 +165,13 @@ impl Client {
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
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]
pub trait Connector: Send + Sync {
pub trait RestConnector: Send + Sync {
fn name(&self) -> String;
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, 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>;
}
impl Debug for dyn Connector {
impl Debug for dyn RestConnector {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
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
**************/
pub struct BitfinexConnector {
bfx: Bitfinex,
rest: bitfinex::api::RestClient,
ws: bitfinex::websockets::WebSocketClient,
}
impl BitfinexConnector {
@ -217,7 +229,8 @@ impl BitfinexConnector {
pub fn new(api_key: &str, api_secret: &str) -> Self {
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>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<O, BoxError>>,
Fut: Future<Output=Result<O, BoxError>>,
{
let response = {
loop {
@ -258,14 +271,14 @@ impl BitfinexConnector {
}
#[async_trait]
impl Connector for BitfinexConnector {
impl RestConnector for BitfinexConnector {
fn name(&self) -> String {
"Bitfinex".into()
"Bitfinex REST".into()
}
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
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
.into_iter()
@ -273,14 +286,14 @@ impl Connector for BitfinexConnector {
.filter(|x: &Position| x.pair() == pair)
.collect();
trace!("\t[PositionManager] Retrieved positions for {}", pair);
trace!("\tRetrieved positions for {}", pair);
Ok((!positions.is_empty()).then_some(positions))
}
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
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)
}
@ -289,7 +302,7 @@ impl Connector for BitfinexConnector {
let symbol_name = BitfinexConnector::format_trading_pair(pair);
let response = BitfinexConnector::retry_nonce(|| {
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0)
self.rest.book.trading_pair(&symbol_name, BookPrecision::P0)
})
.await?;
@ -306,7 +319,7 @@ impl Connector for BitfinexConnector {
}
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())
}
@ -316,7 +329,6 @@ impl Connector for BitfinexConnector {
let amount = order.amount();
let order_form = {
let pre_leverage = {
match order.kind() {
OrderKind::Limit { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
@ -327,13 +339,13 @@ impl Connector for BitfinexConnector {
OrderKind::Stop { price } => {
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())
.with_price_aux_limit(limit_price)?
.with_price_aux_limit(Some(limit_price))?
}
OrderKind::TrailingStop { distance } => {
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
.with_price_trailing(distance)?
.with_price_trailing(Some(distance))?
}
OrderKind::FillOrKill { price } => {
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())
}
}
.with_meta(OrderMeta::new(
.with_meta(Some(OrderMeta::new(
BitfinexConnector::AFFILIATE_CODE.to_string(),
))
};
// 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,
}
)))
// TODO: CHANGEME!
.with_leverage(Some(15))
};
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> {
let cancel_form = order.into();
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()?)
}
@ -379,7 +388,7 @@ impl Connector for BitfinexConnector {
amount: f64,
) -> Result<(), BoxError> {
BitfinexConnector::retry_nonce(|| {
self.bfx.account.transfer_between_wallets(
self.rest.account.transfer_between_wallets(
from.into(),
to.into(),
symbol.to_string(),
@ -396,7 +405,7 @@ impl Connector for BitfinexConnector {
order: &OrderDetails,
) -> Result<Option<Vec<Trade>>, BoxError> {
let response = BitfinexConnector::retry_nonce(|| {
self.bfx
self.rest
.trades
.generated_by_order(order.pair().trading_repr(), order.id())
})
@ -414,7 +423,7 @@ impl Connector for BitfinexConnector {
pair: &SymbolPair,
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
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?;
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> {
let mut fees = vec![];
let accountfees =
BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?;
BitfinexConnector::retry_nonce(|| self.rest.account.account_summary()).await?;
// Derivatives
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 {
fn from(o: &ActiveOrder) -> Self {
Self::from_id(o.id())
@ -511,7 +531,6 @@ impl TryInto<Position> for bitfinex::positions::Position {
}
};
println!("leverage: {}", self.leverage());
Ok(Position::new(
SymbolPair::from_str(self.symbol())?,
state,
@ -603,7 +622,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
}
bitfinex::orders::OrderKind::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!"),
},
bitfinex::orders::OrderKind::TrailingStop
@ -642,7 +661,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
}
bitfinex::orders::OrderKind::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!"),
},
bitfinex::orders::OrderKind::TrailingStop

View File

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

View File

@ -56,14 +56,19 @@ pub struct Event {
}
impl Event {
pub fn new(kind: EventKind, tick: u64, metadata: Option<EventMetadata>) -> Self {
pub fn new(kind: EventKind, tick: u64) -> Self {
Event {
kind,
tick,
metadata,
metadata: None,
}
}
pub fn with_metadata(mut self, metadata: Option<EventMetadata>) -> Self {
self.metadata = metadata;
self
}
fn has_metadata(&self) -> bool {
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 futures_util::stream::TryStreamExt;
use futures_util::StreamExt;
use log::info;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio_tungstenite::accept_async;
use crate::BoxError;
use crate::events::ActorMessage;
#[derive(Debug)]
pub struct FrontendManager {
receiver: Receiver<ActorMessage>,

View File

@ -4,12 +4,13 @@
use std::env;
use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter::{Trace};
use log::error;
use log::LevelFilter::Info;
use tokio::time::Duration;
use crate::bot::BfxBot;
use crate::bot::Rustico;
use crate::connectors::ExchangeDetails;
use crate::currency::Symbol;
use crate::currency::{Symbol, SymbolPair};
mod bot;
mod connectors;
@ -20,13 +21,22 @@ mod managers;
mod models;
mod strategy;
mod ticker;
mod tests;
mod sounds;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), BoxError> {
setup_logger()?;
dotenv::dotenv()?;
if let Err(e) = setup_logger() {
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()
.find(|(k, _v)| k == "API_KEY")
@ -38,14 +48,24 @@ async fn main() -> Result<(), BoxError> {
.ok_or("API_SECRET not set!")?;
let bitfinex = ExchangeDetails::Bitfinex {
api_key: api_key.into(),
api_secret: api_secret.into(),
api_key,
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![Symbol::DERIV_ETH, Symbol::DERIV_BTC],
Symbol::DERIV_USDT,
pairs,
Duration::new(10, 0),
);
@ -63,16 +83,17 @@ fn setup_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(move |out, message, record| {
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()),
message
))
})
.level(Trace)
.filter(|metadata| metadata.target().contains("rustybot"))
.level(Info)
.filter(|metadata| metadata.target().contains("rustico"))
.chain(std::io::stdout())
// .chain(fern::log_file("rustico.log")?)
.chain(fern::log_file("rustico.log")?)
.apply()?;
Ok(())

View File

@ -1,23 +1,22 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::ops::Neg;
use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
use log::{debug, error, info, trace};
use merge::Merge;
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::mpsc::channel;
use tokio::sync::oneshot;
use tokio::time::Duration;
use crate::BoxError;
use crate::connectors::{Client, ExchangeDetails};
use crate::currency::SymbolPair;
use crate::events::{ActionMessage, ActorMessage, Event};
use crate::models::{
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker,
};
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
use crate::BoxError;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PriceTicker};
use crate::sounds::{MARKET_ORDER_PLACED_PATH, play_sound};
use crate::strategy::{MarketEnforce, PositionStrategy, TrailingStop};
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> {
trace!("\t[PositionManager] Updating {}", self.pair);
let opt_active_positions = self.client.active_positions(&self.pair).await?;
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
match opt_active_positions {
@ -258,11 +258,11 @@ impl PositionManager {
let (pos_on_tick, events_on_tick, messages_on_tick) = self
.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
.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_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
******************/
// Position ID: Order ID
pub type TrackedPositionsMap = HashMap<u64, Vec<u64>>;
pub struct OrderManagerHandle {
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 manager = OrderManager::new(receiver, pair, client, strategy);
let manager = OrderManager::new(receiver, pair, client);
tokio::spawn(OrderManagerHandle::run_order_manager(manager));
@ -384,11 +366,9 @@ impl OrderManagerHandle {
pub struct OrderManager {
receiver: Receiver<ActorMessage>,
tracked_positions: TrackedPositionsMap,
orders_map: HashMap<u64, HashSet<ActiveOrder>>,
pair: SymbolPair,
open_orders: Vec<ActiveOrder>,
client: Client,
strategy: Box<dyn OrderStrategy>,
}
impl OrderManager {
@ -396,18 +376,93 @@ impl OrderManager {
receiver: Receiver<ActorMessage>,
pair: SymbolPair,
client: Client,
strategy: Box<dyn OrderStrategy>,
) -> Self {
OrderManager {
receiver,
pair,
open_orders: Vec::new(),
client,
strategy,
tracked_positions: HashMap::new(),
orders_map: Default::default(),
}
}
/*
* 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> {
let (events, messages) = match msg.message {
ActionMessage::Update { .. } => self.update().await?,
@ -429,14 +484,7 @@ impl OrderManager {
pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> {
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();
if let Some(position_orders) = self.orders_map.get(&position_id) {
for order in position_orders {
match self.client.cancel_order(order).await {
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> {
info!("Submiting {}", order_form.kind());
info!("Submitting order: {}", order_form.kind());
let active_order = self.client.submit_order(order_form).await?;
debug!("Adding order to tracked orders.");
// adding strategy to order, if present in the metadata
let active_order = {
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: this seems extremely dirty. Double check!
self.client.submit_order(order_form).await?.with_strategy(metadata.cloned_strategy())
} else {
self.client.submit_order(order_form).await?
}
};
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!
Ok((None, None))
}
@ -513,10 +569,14 @@ impl OrderManager {
position.platform(),
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());
if let Err(e) = self.client.submit_order(&order_form).await {
// submitting order
if let Err(e) = self.submit_order(&order_form).await {
error!(
"Could not submit {} to close position #{}: {}",
order_form.kind(),
@ -535,61 +595,26 @@ impl OrderManager {
pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
debug!("\t[OrderManager] Updating {}", self.pair);
let (res_open_orders, res_order_book) = tokio::join!(
self.client.active_orders(&self.pair),
self.client.order_book(&self.pair)
);
// updating internal orders' mapping from remote
self.update_orders_map_from_remote().await?;
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.
// 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(tracked_orders) = self.all_tracked_orders() {
// since there are open orders, retrieve order book
let order_book = self.client.order_book(&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.order_form().amount().abs() == position.amount().abs());
for active_order in tracked_orders.iter().filter(|x| x.strategy().is_some()) {
let strategy = active_order.strategy().as_ref().unwrap();
// 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 {
trace!(
"Found open order, calling \"{}\" strategy.",
self.strategy.name()
"Found open order with \"{}\" strategy.",
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 {
for m in messages {
@ -600,7 +625,7 @@ impl OrderManager {
self.client.cancel_order(&active_order).await?;
info!("\tSubmitting {}...", order_form.kind());
self.client.submit_order(&order_form).await?;
self.submit_order(&order_form).await?;
info!("Done!");
}
_ => {
@ -612,10 +637,12 @@ impl OrderManager {
}
}
}
}
Ok((None, None))
}
pub fn best_closing_price(&self, position: &Position, order_book: &OrderBook) -> f64 {
let ask = order_book.lowest_ask();
let bid = order_book.highest_bid();
@ -657,12 +684,11 @@ impl PairManager {
order_manager: OrderManagerHandle::new(
pair.clone(),
client.clone(),
Box::new(MarketEnforce::default()),
),
position_manager: PositionManagerHandle::new(
pair,
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::hash::{Hash, Hasher};
use dyn_clone::clone_box;
use crate::connectors::Exchange;
use crate::currency::{Symbol, SymbolPair};
use crate::strategy::OrderStrategy;
/***************
* Prices
@ -148,7 +151,7 @@ impl OrderDetails {
}
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct ActiveOrder {
exchange: Exchange,
id: u64,
@ -158,6 +161,7 @@ pub struct ActiveOrder {
order_form: OrderForm,
creation_timestamp: u64,
update_timestamp: u64,
strategy: Option<Box<dyn OrderStrategy>>,
}
impl ActiveOrder {
@ -178,6 +182,7 @@ impl ActiveOrder {
order_form,
creation_timestamp,
update_timestamp,
strategy: None,
}
}
@ -191,6 +196,16 @@ impl ActiveOrder {
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 {
self.exchange
}
@ -215,6 +230,9 @@ impl ActiveOrder {
pub fn update_timestamp(&self) -> u64 {
self.update_timestamp
}
pub fn strategy(&self) -> &Option<Box<dyn OrderStrategy>> {
&self.strategy
}
}
impl Hash for ActiveOrder {
@ -231,6 +249,22 @@ impl PartialEq 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)]
pub enum TradingPlatform {
Exchange,
@ -261,7 +295,7 @@ pub enum OrderKind {
Limit { price: f64 },
Market,
Stop { price: f64 },
StopLimit { price: f64, limit_price: f64 },
StopLimit { stop_price: f64, limit_price: f64 },
TrailingStop { distance: f64 },
FillOrKill { price: f64 },
ImmediateOrCancel { price: f64 },
@ -285,31 +319,31 @@ impl Display for OrderKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
OrderKind::Limit { price } => {
write!(f, "[{} | Price: {:0.5}]", self.as_str(), 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,)
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
}
OrderKind::StopLimit { price, limit_price } => {
OrderKind::StopLimit { stop_price, limit_price } => {
write!(
f,
"[{} | Price: {:0.5}, Limit Price: {:0.5}]",
"[{} | Stop: {:0.5}, Limit: {:0.5}]",
self.as_str(),
price,
stop_price,
limit_price
)
}
OrderKind::TrailingStop { distance } => {
write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,)
write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance, )
}
OrderKind::FillOrKill { price } => {
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
write!(f, "[{} | Price: {:0.5}]", self.as_str(), 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 {
&self.pair
}
pub fn kind(&self) -> OrderKind {
self.kind
}
pub fn platform(&self) -> &TradingPlatform {
&self.platform
}
pub fn amount(&self) -> f64 {
self.amount
}
pub fn price(&self) -> Option<f64> {
match self.kind {
OrderKind::Limit { price, .. } => Some(price),
OrderKind::Market { .. } => None,
OrderKind::Stop { price, .. } => Some(price),
OrderKind::StopLimit { price, .. } => Some(price),
OrderKind::StopLimit { stop_price: price, .. } => Some(price),
OrderKind::TrailingStop { .. } => None,
OrderKind::FillOrKill { price, .. } => Some(price),
OrderKind::ImmediateOrCancel { price, .. } => Some(price),
}
}
pub fn leverage(&self) -> Option<f64> {
self.leverage
}
pub fn metadata(&self) -> &Option<OrderMetadata> {
&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 {
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 {
pub fn with_position_id(position_id: u64) -> Self {
OrderMetadata {
position_id: Some(position_id),
pub fn new() -> Self {
Self {
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> {
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::fmt::{Debug, Formatter};
use std::ops::Neg;
use dyn_clone::DynClone;
use log::info;
use crate::connectors::Connector;
use crate::BoxError;
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
use crate::managers::OptionUpdate;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
use crate::BoxError;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees};
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
@ -21,12 +23,14 @@ pub trait PositionStrategy: DynClone + Send + Sync {
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
fn post_tick(
&mut self,
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
}
@ -67,8 +71,16 @@ impl Debug for dyn OrderStrategy {
***************/
#[derive(Clone, Debug)]
pub struct HiddenTrailingStop {
pub struct TrailingStop {
// Position ID: stop percentage mapping
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_min_profit: f64,
capital_good_profit: f64,
@ -80,55 +92,96 @@ pub struct HiddenTrailingStop {
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 {
let current_stop_percentage = position.pl_perc() - profit_state_delta;
impl TrailingStop {
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 =
profit_state
{
if current_position.profit_state().is_none() {
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()) {
None => {
self.stop_percentages
.insert(position.id(), current_stop_percentage);
info!(
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%)",
position.profit_state().unwrap(),
position.pl(),
position.pair().quote(),
position.pl_perc()
);
}
Some(existing_threshold) => {
if existing_threshold < &current_stop_percentage {
self.stop_percentages
.insert(position.id(), current_stop_percentage);
}
}
}
}
}
Some(stop_percentage) => {
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)
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 {
let leverage = 5.0;
let leverage = 15.0;
// in percentage
let capital_max_loss = 15.0;
let capital_min_profit = 9.0;
let capital_min_profit = 8.5;
let capital_max_loss = capital_min_profit * 1.9;
let capital_good_profit = capital_min_profit * 2.0;
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 max_loss_percentage = -weighted_max_loss;
HiddenTrailingStop {
TrailingStop {
stop_percentages: Default::default(),
stop_loss_flags: Default::default(),
trail_set_flags: Default::default(),
capital_max_loss,
capital_min_profit,
capital_good_profit,
@ -156,9 +211,10 @@ impl Default for HiddenTrailingStop {
}
}
}
impl PositionStrategy for HiddenTrailingStop {
impl PositionStrategy for TrailingStop {
fn name(&self) -> String {
"Hidden Trailing Stop".into()
"Trailing Stop".into()
}
/// Sets the profit state of an open position
@ -167,20 +223,22 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
_: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let pl_perc = position.pl_perc();
// setting the state of the position based on its profit/loss percentage
let state = {
if pl_perc > self.good_profit_percentage {
PositionProfitState::Profit
Profit
} 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) {
PositionProfitState::BreakEven
BreakEven
} else if (self.max_loss_percentage..0.0).contains(&pl_perc) {
PositionProfitState::Loss
Loss
} else {
PositionProfitState::Critical
Critical
}
};
@ -188,54 +246,53 @@ impl PositionStrategy for HiddenTrailingStop {
let event_metadata = EventMetadata::new(Some(position.id()), None);
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 {
Some(prev) => {
if prev.profit_state() == Some(state) {
return (new_position, None, None);
}
TrailingStop::play_sound_on_state(&prev, &new_position);
}
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(
let event = match state {
PositionProfitState::Critical => {
Event::new(
EventKind::ReachedMaxLoss,
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(events), None)
(new_position, Some(vec![event]), None)
}
fn post_tick(
@ -243,284 +300,89 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position,
_: u64,
_: &HashMap<u64, Position>,
fees: &[TradingFees],
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let close_message = ActionMessage::ClosePosition {
position_id: position.id(),
let taker_fee = fees
.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
if let Some(PositionProfitState::Critical) = position.profit_state() {
info!("Maximum loss reached. Closing position.");
return (position, None, Some(vec![close_message]));
let close_position_orders_msg = ActionMessage::ClosePositionOrders {
position_id: position.id(),
};
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
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]));
messages.push(close_position_msg);
return (position, None, Some(messages));
}
}
self.update_stop_percentage(&position);
(position, None, None)
(position, None, Some(messages))
}
}
// #[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
*/
@ -535,7 +397,7 @@ pub struct MarketEnforce {
impl Default for MarketEnforce {
fn default() -> Self {
Self {
threshold: 1.0 / 15.0,
threshold: 1.2 / 15.0,
}
}
}
@ -554,7 +416,7 @@ impl OrderStrategy for MarketEnforce {
// long
let offer_comparison = {
if order.order_form().amount() > 0.0 {
if order.order_form().is_long() {
order_book.highest_bid()
} else {
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
}
}