Compare commits
No commits in common. "master" and "rust" have entirely different histories.
73
Cargo.lock
generated
73
Cargo.lock
generated
@ -2,9 +2,9 @@
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.14.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
||||
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@ -75,9 +75,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.56"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
|
||||
checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if 1.0.0",
|
||||
@ -109,7 +109,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tungstenite 0.13.0",
|
||||
"tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -237,17 +237,6 @@ 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"
|
||||
@ -802,9 +791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.23.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
|
||||
checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
@ -1170,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
|
||||
|
||||
[[package]]
|
||||
name = "rustico"
|
||||
name = "rustybot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
@ -1179,7 +1168,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"dotenv",
|
||||
"dyn-clone",
|
||||
"ears",
|
||||
"fern",
|
||||
"float-cmp",
|
||||
"futures-retry",
|
||||
@ -1189,7 +1177,7 @@ dependencies = [
|
||||
"regex",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tungstenite 0.12.0",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1353,26 +1341,6 @@ 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"
|
||||
@ -1470,7 +1438,7 @@ dependencies = [
|
||||
"log 0.4.11",
|
||||
"pin-project 1.0.2",
|
||||
"tokio",
|
||||
"tungstenite 0.12.0",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1551,27 +1519,6 @@ 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"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "rustico"
|
||||
name = "rustybot"
|
||||
version = "0.1.0"
|
||||
authors = ["Giulio De Pasquale <depasquale@giugl.io>"]
|
||||
edition = "2018"
|
||||
@ -22,5 +22,4 @@ merge = "0.1"
|
||||
futures-retry = "0.6"
|
||||
tungstenite = "0.12"
|
||||
tokio-tungstenite = "0.13"
|
||||
dotenv = "0.15"
|
||||
ears = "0.8.0"
|
||||
dotenv = "0.15"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21
src/bot.rs
21
src/bot.rs
@ -3,32 +3,37 @@ 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 Rustico {
|
||||
pub struct BfxBot {
|
||||
ticker: Ticker,
|
||||
exchange_managers: Vec<ExchangeManager>,
|
||||
frontend_connector: FrontendManagerHandle,
|
||||
}
|
||||
|
||||
impl Rustico {
|
||||
// TODO: change constructor to take SymbolPairs and not Symbol
|
||||
impl BfxBot {
|
||||
pub fn new(
|
||||
exchanges: Vec<ExchangeDetails>,
|
||||
trading_pairs: Vec<SymbolPair>,
|
||||
trading_symbols: Vec<Symbol>,
|
||||
quote: Symbol,
|
||||
tick_duration: Duration,
|
||||
) -> Self {
|
||||
let exchange_managers = exchanges
|
||||
let pairs: Vec<_> = trading_symbols
|
||||
.iter()
|
||||
.map(|x| ExchangeManager::new(x, &trading_pairs))
|
||||
.map(|x| SymbolPair::new(quote.clone(), x.clone()))
|
||||
.collect();
|
||||
|
||||
Rustico {
|
||||
let exchange_managers = exchanges
|
||||
.iter()
|
||||
.map(|x| ExchangeManager::new(x, &pairs))
|
||||
.collect();
|
||||
|
||||
BfxBot {
|
||||
ticker: Ticker::new(tick_duration),
|
||||
exchange_managers,
|
||||
frontend_connector: FrontendManagerHandle::new(),
|
||||
|
@ -4,23 +4,22 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bitfinex::api::RestClient;
|
||||
use bitfinex::api::Bitfinex;
|
||||
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 {
|
||||
@ -37,7 +36,7 @@ pub enum ExchangeDetails {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
exchange: Exchange,
|
||||
inner: Arc<Box<dyn RestConnector>>,
|
||||
inner: Arc<Box<dyn Connector>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@ -124,7 +123,7 @@ impl Client {
|
||||
.active_orders(pair)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| x.pair() == pair)
|
||||
.filter(|x| &x.pair() == &pair)
|
||||
.collect())
|
||||
}
|
||||
|
||||
@ -165,13 +164,10 @@ 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 RestConnector: Send + Sync {
|
||||
pub trait Connector: 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>;
|
||||
@ -187,7 +183,7 @@ pub trait RestConnector: Send + Sync {
|
||||
amount: f64,
|
||||
) -> Result<(), BoxError>;
|
||||
async fn trades_from_order(&self, order: &OrderDetails)
|
||||
-> Result<Option<Vec<Trade>>, BoxError>;
|
||||
-> Result<Option<Vec<Trade>>, BoxError>;
|
||||
async fn orders_history(
|
||||
&self,
|
||||
pair: &SymbolPair,
|
||||
@ -195,26 +191,18 @@ pub trait RestConnector: Send + Sync {
|
||||
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>;
|
||||
}
|
||||
|
||||
impl Debug for dyn RestConnector {
|
||||
impl Debug for dyn Connector {
|
||||
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 {
|
||||
rest: bitfinex::api::RestClient,
|
||||
ws: bitfinex::websockets::WebSocketClient,
|
||||
bfx: Bitfinex,
|
||||
}
|
||||
|
||||
impl BitfinexConnector {
|
||||
@ -229,8 +217,7 @@ impl BitfinexConnector {
|
||||
|
||||
pub fn new(api_key: &str, api_secret: &str) -> Self {
|
||||
BitfinexConnector {
|
||||
rest: RestClient::new(Some(api_key.into()), Some(api_secret.into())),
|
||||
ws: WebSocketClient::new(),
|
||||
bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,9 +235,9 @@ impl BitfinexConnector {
|
||||
// the function may fail due to concurrent signed requests
|
||||
// parsed in different times by the server
|
||||
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output=Result<O, BoxError>>,
|
||||
where
|
||||
F: FnMut() -> Fut,
|
||||
Fut: Future<Output = Result<O, BoxError>>,
|
||||
{
|
||||
let response = {
|
||||
loop {
|
||||
@ -271,14 +258,14 @@ impl BitfinexConnector {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RestConnector for BitfinexConnector {
|
||||
impl Connector for BitfinexConnector {
|
||||
fn name(&self) -> String {
|
||||
"Bitfinex REST".into()
|
||||
"Bitfinex".into()
|
||||
}
|
||||
|
||||
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
|
||||
let active_positions =
|
||||
BitfinexConnector::retry_nonce(|| self.rest.positions.active_positions()).await?;
|
||||
BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?;
|
||||
|
||||
let positions: Vec<_> = active_positions
|
||||
.into_iter()
|
||||
@ -286,14 +273,14 @@ impl RestConnector for BitfinexConnector {
|
||||
.filter(|x: &Position| x.pair() == pair)
|
||||
.collect();
|
||||
|
||||
trace!("\tRetrieved positions for {}", pair);
|
||||
trace!("\t[PositionManager] Retrieved 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.rest.ticker.trading_pair(symbol_name).await?;
|
||||
let ticker: TradingPairTicker = self.bfx.ticker.trading_pair(symbol_name).await?;
|
||||
|
||||
Ok(ticker)
|
||||
}
|
||||
@ -302,9 +289,9 @@ impl RestConnector for BitfinexConnector {
|
||||
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
||||
|
||||
let response = BitfinexConnector::retry_nonce(|| {
|
||||
self.rest.book.trading_pair(&symbol_name, BookPrecision::P0)
|
||||
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0)
|
||||
})
|
||||
.await?;
|
||||
.await?;
|
||||
|
||||
let entries = response
|
||||
.into_iter()
|
||||
@ -319,7 +306,7 @@ impl RestConnector for BitfinexConnector {
|
||||
}
|
||||
|
||||
async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
|
||||
let response = BitfinexConnector::retry_nonce(|| self.rest.orders.active_orders()).await?;
|
||||
let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?;
|
||||
|
||||
Ok(response.iter().map(Into::into).collect())
|
||||
}
|
||||
@ -329,53 +316,57 @@ impl RestConnector for BitfinexConnector {
|
||||
let amount = order.amount();
|
||||
|
||||
let order_form = {
|
||||
match order.kind() {
|
||||
OrderKind::Limit { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
let pre_leverage = {
|
||||
match order.kind() {
|
||||
OrderKind::Limit { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
OrderKind::Market => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
||||
}
|
||||
OrderKind::Stop { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
OrderKind::StopLimit { price, limit_price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
.with_price_aux_limit(limit_price)?
|
||||
}
|
||||
OrderKind::TrailingStop { distance } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
||||
.with_price_trailing(distance)?
|
||||
}
|
||||
OrderKind::FillOrKill { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
OrderKind::ImmediateOrCancel { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
}
|
||||
OrderKind::Market => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
||||
}
|
||||
OrderKind::Stop { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
OrderKind::StopLimit { stop_price: price, limit_price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
.with_price_aux_limit(Some(limit_price))?
|
||||
}
|
||||
OrderKind::TrailingStop { distance } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
||||
.with_price_trailing(Some(distance))?
|
||||
}
|
||||
OrderKind::FillOrKill { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
OrderKind::ImmediateOrCancel { price } => {
|
||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||
}
|
||||
}
|
||||
.with_meta(Some(OrderMeta::new(
|
||||
.with_meta(OrderMeta::new(
|
||||
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 =
|
||||
BitfinexConnector::retry_nonce(|| self.rest.orders.submit_order(&order_form)).await?;
|
||||
BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?;
|
||||
|
||||
// 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)))
|
||||
Ok((&response).try_into()?)
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
|
||||
let cancel_form = order.into();
|
||||
|
||||
let response =
|
||||
BitfinexConnector::retry_nonce(|| self.rest.orders.cancel_order(&cancel_form)).await?;
|
||||
BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?;
|
||||
|
||||
Ok((&response).try_into()?)
|
||||
}
|
||||
@ -388,14 +379,14 @@ impl RestConnector for BitfinexConnector {
|
||||
amount: f64,
|
||||
) -> Result<(), BoxError> {
|
||||
BitfinexConnector::retry_nonce(|| {
|
||||
self.rest.account.transfer_between_wallets(
|
||||
self.bfx.account.transfer_between_wallets(
|
||||
from.into(),
|
||||
to.into(),
|
||||
symbol.to_string(),
|
||||
amount,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -405,11 +396,11 @@ impl RestConnector for BitfinexConnector {
|
||||
order: &OrderDetails,
|
||||
) -> Result<Option<Vec<Trade>>, BoxError> {
|
||||
let response = BitfinexConnector::retry_nonce(|| {
|
||||
self.rest
|
||||
self.bfx
|
||||
.trades
|
||||
.generated_by_order(order.pair().trading_repr(), order.id())
|
||||
})
|
||||
.await?;
|
||||
.await?;
|
||||
|
||||
if response.is_empty() {
|
||||
Ok(None)
|
||||
@ -423,7 +414,7 @@ impl RestConnector for BitfinexConnector {
|
||||
pair: &SymbolPair,
|
||||
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
||||
let response =
|
||||
BitfinexConnector::retry_nonce(|| self.rest.orders.history(Some(pair.trading_repr())))
|
||||
BitfinexConnector::retry_nonce(|| self.bfx.orders.history(Some(pair.trading_repr())))
|
||||
.await?;
|
||||
let mapped_vec: Vec<_> = response.iter().map(Into::into).collect();
|
||||
|
||||
@ -433,7 +424,7 @@ impl RestConnector for BitfinexConnector {
|
||||
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> {
|
||||
let mut fees = vec![];
|
||||
let accountfees =
|
||||
BitfinexConnector::retry_nonce(|| self.rest.account.account_summary()).await?;
|
||||
BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?;
|
||||
|
||||
// Derivatives
|
||||
let derivative_taker = TradingFees::Taker {
|
||||
@ -475,17 +466,6 @@ impl RestConnector 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())
|
||||
@ -506,8 +486,8 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder {
|
||||
response.mts_create(),
|
||||
response.mts_update(),
|
||||
)
|
||||
.with_group_id(response.gid())
|
||||
.with_client_id(Some(response.cid())))
|
||||
.with_group_id(response.gid())
|
||||
.with_client_id(Some(response.cid())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,6 +511,7 @@ impl TryInto<Position> for bitfinex::positions::Position {
|
||||
}
|
||||
};
|
||||
|
||||
println!("leverage: {}", self.leverage());
|
||||
Ok(Position::new(
|
||||
SymbolPair::from_str(self.symbol())?,
|
||||
state,
|
||||
@ -543,8 +524,8 @@ impl TryInto<Position> for bitfinex::positions::Position {
|
||||
platform,
|
||||
self.leverage(),
|
||||
)
|
||||
.with_creation_date(self.mts_create())
|
||||
.with_creation_update(self.mts_update()))
|
||||
.with_creation_date(self.mts_create())
|
||||
.with_creation_update(self.mts_update()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -622,7 +603,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
|
||||
}
|
||||
bitfinex::orders::OrderKind::StopLimit
|
||||
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
||||
stop_price: response.price(),
|
||||
price: response.price(),
|
||||
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
||||
},
|
||||
bitfinex::orders::OrderKind::TrailingStop
|
||||
@ -661,7 +642,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
|
||||
}
|
||||
bitfinex::orders::OrderKind::StopLimit
|
||||
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
||||
stop_price: response.price(),
|
||||
price: response.price(),
|
||||
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
||||
},
|
||||
bitfinex::orders::OrderKind::TrailingStop
|
||||
@ -694,8 +675,8 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder {
|
||||
order.creation_timestamp(),
|
||||
order.update_timestamp(),
|
||||
)
|
||||
.with_client_id(Some(order.client_id()))
|
||||
.with_group_id(order.group_id())
|
||||
.with_client_id(Some(order.client_id()))
|
||||
.with_group_id(order.group_id())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,8 @@ pub struct Symbol {
|
||||
}
|
||||
|
||||
impl<S> From<S> for Symbol
|
||||
where
|
||||
S: Into<String>,
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from(item: S) -> Self {
|
||||
Symbol::new(item.into())
|
||||
@ -31,8 +31,6 @@ 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");
|
||||
@ -76,8 +74,8 @@ pub struct SymbolPair {
|
||||
}
|
||||
|
||||
impl SymbolPair {
|
||||
pub fn new(base: Symbol, quote: Symbol) -> Self {
|
||||
SymbolPair { base, quote }
|
||||
pub fn new(quote: Symbol, base: Symbol) -> Self {
|
||||
SymbolPair { quote, base }
|
||||
}
|
||||
pub fn trading_repr(&self) -> String {
|
||||
format!("t{}{}", self.base, self.quote)
|
||||
|
@ -56,19 +56,14 @@ pub struct Event {
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn new(kind: EventKind, tick: u64) -> Self {
|
||||
pub fn new(kind: EventKind, tick: u64, metadata: Option<EventMetadata>) -> Self {
|
||||
Event {
|
||||
kind,
|
||||
tick,
|
||||
metadata: None,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_metadata(mut self, metadata: Option<EventMetadata>) -> Self {
|
||||
self.metadata = metadata;
|
||||
self
|
||||
}
|
||||
|
||||
fn has_metadata(&self) -> bool {
|
||||
self.metadata.is_some()
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
use std::net::SocketAddr;
|
||||
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 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;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_tungstenite::accept_async;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FrontendManager {
|
||||
|
51
src/main.rs
51
src/main.rs
@ -4,13 +4,12 @@
|
||||
use std::env;
|
||||
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use log::error;
|
||||
use log::LevelFilter::Info;
|
||||
use log::LevelFilter::{Trace};
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::bot::Rustico;
|
||||
use crate::bot::BfxBot;
|
||||
use crate::connectors::ExchangeDetails;
|
||||
use crate::currency::{Symbol, SymbolPair};
|
||||
use crate::currency::Symbol;
|
||||
|
||||
mod bot;
|
||||
mod connectors;
|
||||
@ -21,22 +20,13 @@ 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> {
|
||||
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());
|
||||
}
|
||||
setup_logger()?;
|
||||
dotenv::dotenv()?;
|
||||
|
||||
let api_key = env::vars()
|
||||
.find(|(k, _v)| k == "API_KEY")
|
||||
@ -48,24 +38,14 @@ async fn main() -> Result<(), BoxError> {
|
||||
.ok_or("API_SECRET not set!")?;
|
||||
|
||||
let bitfinex = ExchangeDetails::Bitfinex {
|
||||
api_key,
|
||||
api_secret,
|
||||
api_key: api_key.into(),
|
||||
api_secret: api_secret.into(),
|
||||
};
|
||||
|
||||
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(
|
||||
let mut bot = BfxBot::new(
|
||||
vec![bitfinex],
|
||||
pairs,
|
||||
vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC],
|
||||
Symbol::DERIV_USDT,
|
||||
Duration::new(10, 0),
|
||||
);
|
||||
|
||||
@ -83,17 +63,16 @@ fn setup_logger() -> Result<(), fern::InitError> {
|
||||
fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{} | [{}][{}] | {}",
|
||||
chrono::Local::now().format("[%d/%m/%Y][%H:%M:%S]"),
|
||||
record.target().strip_prefix("rustico::").unwrap_or("rustico"),
|
||||
"[{}][{}] {}",
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(Info)
|
||||
.filter(|metadata| metadata.target().contains("rustico"))
|
||||
.level(Trace)
|
||||
.filter(|metadata| metadata.target().contains("rustybot"))
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("rustico.log")?)
|
||||
// .chain(fern::log_file("rustico.log")?)
|
||||
.apply()?;
|
||||
|
||||
Ok(())
|
||||
|
282
src/managers.rs
282
src/managers.rs
@ -1,22 +1,23 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
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::{Receiver, Sender};
|
||||
use tokio::sync::mpsc::channel;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
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, OrderMetadata, Position, PriceTicker};
|
||||
use crate::sounds::{MARKET_ORDER_PLACED_PATH, play_sound};
|
||||
use crate::strategy::{MarketEnforce, PositionStrategy, TrailingStop};
|
||||
use crate::models::{
|
||||
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker,
|
||||
};
|
||||
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
|
||||
use crate::BoxError;
|
||||
|
||||
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
||||
|
||||
@ -233,9 +234,8 @@ 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, &fees);
|
||||
.on_tick(position, self.current_tick(), &self.positions_history);
|
||||
|
||||
let (pos_post_tick, events_post_tick, messages_post_tick) = self
|
||||
.strategy
|
||||
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history, &fees);
|
||||
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history);
|
||||
|
||||
events.merge(events_on_tick);
|
||||
events.merge(events_post_tick);
|
||||
@ -280,12 +280,30 @@ 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>,
|
||||
}
|
||||
@ -311,10 +329,10 @@ impl OrderManagerHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(pair: SymbolPair, client: Client) -> Self {
|
||||
pub fn new(pair: SymbolPair, client: Client, strategy: Box<dyn OrderStrategy>) -> Self {
|
||||
let (sender, receiver) = channel(1);
|
||||
|
||||
let manager = OrderManager::new(receiver, pair, client);
|
||||
let manager = OrderManager::new(receiver, pair, client, strategy);
|
||||
|
||||
tokio::spawn(OrderManagerHandle::run_order_manager(manager));
|
||||
|
||||
@ -366,9 +384,11 @@ impl OrderManagerHandle {
|
||||
|
||||
pub struct OrderManager {
|
||||
receiver: Receiver<ActorMessage>,
|
||||
orders_map: HashMap<u64, HashSet<ActiveOrder>>,
|
||||
tracked_positions: TrackedPositionsMap,
|
||||
pair: SymbolPair,
|
||||
open_orders: Vec<ActiveOrder>,
|
||||
client: Client,
|
||||
strategy: Box<dyn OrderStrategy>,
|
||||
}
|
||||
|
||||
impl OrderManager {
|
||||
@ -376,93 +396,18 @@ impl OrderManager {
|
||||
receiver: Receiver<ActorMessage>,
|
||||
pair: SymbolPair,
|
||||
client: Client,
|
||||
strategy: Box<dyn OrderStrategy>,
|
||||
) -> Self {
|
||||
OrderManager {
|
||||
receiver,
|
||||
pair,
|
||||
open_orders: Vec::new(),
|
||||
client,
|
||||
orders_map: Default::default(),
|
||||
strategy,
|
||||
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> {
|
||||
let (events, messages) = match msg.message {
|
||||
ActionMessage::Update { .. } => self.update().await?,
|
||||
@ -484,7 +429,14 @@ 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.orders_map.get(&position_id) {
|
||||
if let Some(position_orders) = self.tracked_positions.get(&position_id) {
|
||||
// retrieving open orders
|
||||
let open_orders = self.client.active_orders(&self.pair).await?;
|
||||
let position_orders: Vec<_> = position_orders
|
||||
.iter()
|
||||
.filter_map(|&x| open_orders.iter().find(|y| y.id() == x))
|
||||
.collect();
|
||||
|
||||
for order in position_orders {
|
||||
match self.client.cancel_order(order).await {
|
||||
Ok(_) => info!("Order #{} closed successfully.", order.id()),
|
||||
@ -498,33 +450,25 @@ impl OrderManager {
|
||||
}
|
||||
|
||||
pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result<OptionUpdate, BoxError> {
|
||||
info!("Submitting order: {}", order_form.kind());
|
||||
info!("Submiting {}", order_form.kind());
|
||||
|
||||
// adding strategy to order, if present in the metadata
|
||||
let active_order = {
|
||||
if let Some(metadata) = order_form.metadata() {
|
||||
// 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?
|
||||
}
|
||||
};
|
||||
let active_order = self.client.submit_order(order_form).await?;
|
||||
|
||||
debug!("Adding order to tracked orders.");
|
||||
if let Some(metadata) = order_form.metadata() {
|
||||
if let Some(position_id) = metadata.position_id() {
|
||||
debug!("Adding order to tracked orders.");
|
||||
|
||||
if !self.add_to_orders_map(position_id, active_order) {
|
||||
error!("Failed while adding order to internal mapping.");
|
||||
};
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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))
|
||||
}
|
||||
@ -569,14 +513,10 @@ impl OrderManager {
|
||||
position.platform(),
|
||||
position.amount().neg(),
|
||||
)
|
||||
.with_leverage(Some(position.leverage()))
|
||||
.with_metadata(Some(OrderMetadata::new()
|
||||
.with_strategy(Some(Box::new(MarketEnforce::default())))
|
||||
.with_position_id(Some(position.id())))
|
||||
);
|
||||
.with_leverage(Some(position.leverage()));
|
||||
|
||||
// submitting order
|
||||
if let Err(e) = self.submit_order(&order_form).await {
|
||||
info!("Submitting {} order", order_form.kind());
|
||||
if let Err(e) = self.client.submit_order(&order_form).await {
|
||||
error!(
|
||||
"Could not submit {} to close position #{}: {}",
|
||||
order_form.kind(),
|
||||
@ -595,43 +535,47 @@ impl OrderManager {
|
||||
pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
|
||||
debug!("\t[OrderManager] Updating {}", self.pair);
|
||||
|
||||
// updating internal orders' mapping from remote
|
||||
self.update_orders_map_from_remote().await?;
|
||||
let (res_open_orders, res_order_book) = tokio::join!(
|
||||
self.client.active_orders(&self.pair),
|
||||
self.client.order_book(&self.pair)
|
||||
);
|
||||
|
||||
// calling strategies for the orders and collecting resulting messages
|
||||
let _orders_messages: HashMap<&ActiveOrder, Vec<ActionMessage>> = HashMap::new();
|
||||
let (open_orders, order_book) = (res_open_orders?, res_order_book?);
|
||||
|
||||
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?;
|
||||
// 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?;
|
||||
|
||||
for active_order in tracked_orders.iter().filter(|x| x.strategy().is_some()) {
|
||||
let strategy = active_order.strategy().as_ref().unwrap();
|
||||
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());
|
||||
|
||||
trace!(
|
||||
"Found open order with \"{}\" strategy.",
|
||||
strategy.name()
|
||||
);
|
||||
|
||||
// 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 {
|
||||
match m {
|
||||
ActionMessage::SubmitOrder { order: order_form } => {
|
||||
info!("Closing open order...");
|
||||
info!("\tCancelling open order #{}", &active_order.id());
|
||||
self.client.cancel_order(&active_order).await?;
|
||||
|
||||
info!("\tSubmitting {}...", order_form.kind());
|
||||
self.submit_order(&order_form).await?;
|
||||
info!("Done!");
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!(
|
||||
"Received unsupported message from order strategy. Unimplemented."
|
||||
)
|
||||
None => {
|
||||
trace!(
|
||||
"Mapped order #{} to position #{}",
|
||||
position.id(),
|
||||
matching_order.id()
|
||||
);
|
||||
self.tracked_positions
|
||||
.insert(position.id(), vec![matching_order.id()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -639,10 +583,39 @@ impl OrderManager {
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
if let Some(messages) = strat_messages {
|
||||
for m in messages {
|
||||
match m {
|
||||
ActionMessage::SubmitOrder { order: order_form } => {
|
||||
info!("Closing open order...");
|
||||
info!("\tCancelling open order #{}", &active_order.id());
|
||||
self.client.cancel_order(&active_order).await?;
|
||||
|
||||
info!("\tSubmitting {}...", order_form.kind());
|
||||
self.client.submit_order(&order_form).await?;
|
||||
info!("Done!");
|
||||
}
|
||||
_ => {
|
||||
debug!(
|
||||
"Received unsupported message from order strategy. Unimplemented."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@ -684,11 +657,12 @@ impl PairManager {
|
||||
order_manager: OrderManagerHandle::new(
|
||||
pair.clone(),
|
||||
client.clone(),
|
||||
Box::new(MarketEnforce::default()),
|
||||
),
|
||||
position_manager: PositionManagerHandle::new(
|
||||
pair,
|
||||
client,
|
||||
Box::new(TrailingStop::default()),
|
||||
Box::new(HiddenTrailingStop::default()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
111
src/models.rs
111
src/models.rs
@ -2,11 +2,8 @@ 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
|
||||
@ -151,7 +148,7 @@ impl OrderDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ActiveOrder {
|
||||
exchange: Exchange,
|
||||
id: u64,
|
||||
@ -161,7 +158,6 @@ pub struct ActiveOrder {
|
||||
order_form: OrderForm,
|
||||
creation_timestamp: u64,
|
||||
update_timestamp: u64,
|
||||
strategy: Option<Box<dyn OrderStrategy>>,
|
||||
}
|
||||
|
||||
impl ActiveOrder {
|
||||
@ -182,7 +178,6 @@ impl ActiveOrder {
|
||||
order_form,
|
||||
creation_timestamp,
|
||||
update_timestamp,
|
||||
strategy: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,16 +191,6 @@ 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
|
||||
}
|
||||
@ -230,9 +215,6 @@ 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 {
|
||||
@ -249,22 +231,6 @@ 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,
|
||||
@ -295,7 +261,7 @@ pub enum OrderKind {
|
||||
Limit { price: f64 },
|
||||
Market,
|
||||
Stop { price: f64 },
|
||||
StopLimit { stop_price: f64, limit_price: f64 },
|
||||
StopLimit { price: f64, limit_price: f64 },
|
||||
TrailingStop { distance: f64 },
|
||||
FillOrKill { price: f64 },
|
||||
ImmediateOrCancel { price: f64 },
|
||||
@ -319,31 +285,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 { stop_price, limit_price } => {
|
||||
OrderKind::StopLimit { price, limit_price } => {
|
||||
write!(
|
||||
f,
|
||||
"[{} | Stop: {:0.5}, Limit: {:0.5}]",
|
||||
"[{} | Price: {:0.5}, Limit Price: {:0.5}]",
|
||||
self.as_str(),
|
||||
stop_price,
|
||||
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,)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -389,90 +355,55 @@ 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 { stop_price: price, .. } => Some(price),
|
||||
OrderKind::StopLimit { 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)]
|
||||
#[derive(Debug, Clone)]
|
||||
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 new() -> Self {
|
||||
Self {
|
||||
position_id: None,
|
||||
strategy: None,
|
||||
pub fn with_position_id(position_id: u64) -> Self {
|
||||
OrderMetadata {
|
||||
position_id: Some(position_id),
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/***************
|
||||
|
@ -1,19 +0,0 @@
|
||||
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); }
|
||||
}
|
||||
});
|
||||
}
|
556
src/strategy.rs
556
src/strategy.rs
@ -1,16 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::ops::Neg;
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
use log::info;
|
||||
|
||||
use crate::BoxError;
|
||||
use crate::connectors::Connector;
|
||||
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
|
||||
use crate::managers::OptionUpdate;
|
||||
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};
|
||||
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
|
||||
use crate::BoxError;
|
||||
|
||||
/***************
|
||||
* DEFINITIONS
|
||||
@ -23,14 +21,12 @@ 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>>);
|
||||
}
|
||||
|
||||
@ -71,16 +67,8 @@ impl Debug for dyn OrderStrategy {
|
||||
***************/
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrailingStop {
|
||||
// Position ID: stop percentage mapping
|
||||
pub struct HiddenTrailingStop {
|
||||
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,
|
||||
@ -92,96 +80,55 @@ pub struct TrailingStop {
|
||||
max_loss_percentage: f64,
|
||||
}
|
||||
|
||||
|
||||
impl TrailingStop {
|
||||
fn play_sound_on_state(prev_position: &Position, current_position: &Position) {
|
||||
if prev_position.profit_state().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
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 => {
|
||||
info!(
|
||||
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%)",
|
||||
position.profit_state().unwrap(),
|
||||
position.pl(),
|
||||
position.pair().quote(),
|
||||
position.pl_perc()
|
||||
);
|
||||
}
|
||||
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(),
|
||||
stop_percentage
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 => self.min_profit_trailing_delta,
|
||||
PositionProfitState::Profit => self.good_profit_trailing_delta,
|
||||
_ => return
|
||||
PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta),
|
||||
PositionProfitState::Profit => Some(self.good_profit_trailing_delta),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let current_trailing_delta = position.pl_perc() - profit_state_delta;
|
||||
if let Some(profit_state_delta) = profit_state_delta {
|
||||
let current_stop_percentage = 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 < ¤t_trailing_delta {
|
||||
self.stop_percentages
|
||||
.insert(position.id(), current_trailing_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);
|
||||
}
|
||||
Some(existing_threshold) => {
|
||||
if existing_threshold < ¤t_stop_percentage {
|
||||
self.stop_percentages
|
||||
.insert(position.id(), current_stop_percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
|
||||
position.profit_state().unwrap(),
|
||||
position.pl(),
|
||||
position.pair().quote(),
|
||||
position.pl_perc(),
|
||||
self.stop_percentages.get(&position.id()).unwrap_or(&0.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TrailingStop {
|
||||
impl Default for HiddenTrailingStop {
|
||||
fn default() -> Self {
|
||||
let leverage = 15.0;
|
||||
let leverage = 5.0;
|
||||
|
||||
// in percentage
|
||||
let capital_min_profit = 8.5;
|
||||
let capital_max_loss = capital_min_profit * 1.9;
|
||||
let capital_max_loss = 15.0;
|
||||
let capital_min_profit = 9.0;
|
||||
let capital_good_profit = capital_min_profit * 2.0;
|
||||
|
||||
let weighted_min_profit = capital_min_profit / leverage;
|
||||
@ -195,10 +142,8 @@ impl Default for TrailingStop {
|
||||
let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
|
||||
let max_loss_percentage = -weighted_max_loss;
|
||||
|
||||
TrailingStop {
|
||||
HiddenTrailingStop {
|
||||
stop_percentages: Default::default(),
|
||||
stop_loss_flags: Default::default(),
|
||||
trail_set_flags: Default::default(),
|
||||
capital_max_loss,
|
||||
capital_min_profit,
|
||||
capital_good_profit,
|
||||
@ -211,10 +156,9 @@ impl Default for TrailingStop {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PositionStrategy for TrailingStop {
|
||||
impl PositionStrategy for HiddenTrailingStop {
|
||||
fn name(&self) -> String {
|
||||
"Trailing Stop".into()
|
||||
"Hidden Trailing Stop".into()
|
||||
}
|
||||
|
||||
/// Sets the profit state of an open position
|
||||
@ -223,22 +167,20 @@ impl PositionStrategy for TrailingStop {
|
||||
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 {
|
||||
Profit
|
||||
PositionProfitState::Profit
|
||||
} else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
|
||||
MinimumProfit
|
||||
PositionProfitState::MinimumProfit
|
||||
} else if (0.0..self.min_profit_percentage).contains(&pl_perc) {
|
||||
BreakEven
|
||||
PositionProfitState::BreakEven
|
||||
} else if (self.max_loss_percentage..0.0).contains(&pl_perc) {
|
||||
Loss
|
||||
PositionProfitState::Loss
|
||||
} else {
|
||||
Critical
|
||||
PositionProfitState::Critical
|
||||
}
|
||||
};
|
||||
|
||||
@ -246,53 +188,54 @@ impl PositionStrategy for TrailingStop {
|
||||
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 event = match state {
|
||||
PositionProfitState::Critical => {
|
||||
Event::new(
|
||||
EventKind::ReachedMaxLoss,
|
||||
current_tick,
|
||||
)
|
||||
}
|
||||
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(
|
||||
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),
|
||||
));
|
||||
}
|
||||
}.with_metadata(Some(event_metadata));
|
||||
|
||||
(new_position, Some(vec![event]), None)
|
||||
events
|
||||
};
|
||||
|
||||
(new_position, Some(events), None)
|
||||
}
|
||||
|
||||
fn post_tick(
|
||||
@ -300,89 +243,284 @@ impl PositionStrategy for TrailingStop {
|
||||
position: Position,
|
||||
_: u64,
|
||||
_: &HashMap<u64, Position>,
|
||||
fees: &[TradingFees],
|
||||
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
let close_position_orders_msg = ActionMessage::ClosePositionOrders {
|
||||
let close_message = ActionMessage::ClosePosition {
|
||||
position_id: position.id(),
|
||||
};
|
||||
let close_position_msg = 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 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.");
|
||||
messages.push(close_position_msg);
|
||||
return (position, None, Some(messages));
|
||||
return (position, None, Some(vec![close_message]));
|
||||
}
|
||||
}
|
||||
|
||||
(position, None, Some(messages))
|
||||
self.update_stop_percentage(&position);
|
||||
|
||||
(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 < ¤t_stop_percentage {
|
||||
// self.stop_percentages
|
||||
// .insert(position.id(), current_stop_percentage);
|
||||
//
|
||||
// trace!("Updating threshold, asking order manager to cancel previous orders.");
|
||||
// order_form = Some(
|
||||
// OrderForm::new(
|
||||
// position.pair().clone(),
|
||||
// OrderKind::Limit {
|
||||
// price: position.base_price() * price_percentage_delta,
|
||||
// },
|
||||
// position.platform(),
|
||||
// position.amount().neg(),
|
||||
// )
|
||||
// .with_metadata(OrderMetadata::with_position_id(position.id())),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// info!(
|
||||
// "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
|
||||
// position.profit_state().unwrap(),
|
||||
// position.pl(),
|
||||
// position.pair().quote(),
|
||||
// position.pl_perc(),
|
||||
// self.stop_percentages.get(&position.id()).unwrap_or(&0.0)
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// order_form
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl Default for TrailingStop {
|
||||
// fn default() -> Self {
|
||||
// let leverage = 5.0;
|
||||
//
|
||||
// // in percentage
|
||||
// let capital_max_loss = 15.0;
|
||||
// let capital_min_profit = 1.0;
|
||||
// let capital_good_profit = 6.0;
|
||||
//
|
||||
// let weighted_min_profit = capital_min_profit / leverage;
|
||||
// let weighted_good_profit = capital_good_profit / leverage;
|
||||
// let weighted_max_loss = capital_max_loss / leverage;
|
||||
//
|
||||
// let min_profit_trailing_delta = weighted_min_profit * 0.2;
|
||||
// let good_profit_trailing_delta = weighted_good_profit * 0.08;
|
||||
//
|
||||
// let min_profit_percentage = weighted_min_profit + min_profit_trailing_delta;
|
||||
// let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
|
||||
// let max_loss_percentage = -weighted_max_loss;
|
||||
//
|
||||
// TrailingStop {
|
||||
// stop_percentages: Default::default(),
|
||||
// capital_max_loss,
|
||||
// capital_min_profit,
|
||||
// capital_good_profit,
|
||||
// min_profit_trailing_delta,
|
||||
// good_profit_trailing_delta,
|
||||
// leverage,
|
||||
// min_profit_percentage,
|
||||
// good_profit_percentage,
|
||||
// max_loss_percentage,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// impl PositionStrategy for TrailingStop {
|
||||
// fn name(&self) -> String {
|
||||
// "Trailing Stop".into()
|
||||
// }
|
||||
//
|
||||
// /// Sets the profit state of an open position
|
||||
// fn on_tick(
|
||||
// &mut self,
|
||||
// position: Position,
|
||||
// current_tick: u64,
|
||||
// positions_history: &HashMap<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
|
||||
*/
|
||||
@ -397,7 +535,7 @@ pub struct MarketEnforce {
|
||||
impl Default for MarketEnforce {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 1.2 / 15.0,
|
||||
threshold: 1.0 / 15.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -416,7 +554,7 @@ impl OrderStrategy for MarketEnforce {
|
||||
|
||||
// long
|
||||
let offer_comparison = {
|
||||
if order.order_form().is_long() {
|
||||
if order.order_form().amount() > 0.0 {
|
||||
order_book.highest_bid()
|
||||
} else {
|
||||
order_book.lowest_ask()
|
||||
@ -439,8 +577,8 @@ impl OrderStrategy for MarketEnforce {
|
||||
*order.order_form().platform(),
|
||||
order.order_form().amount(),
|
||||
)
|
||||
.with_leverage(order.order_form().leverage())
|
||||
.with_metadata(order.order_form().metadata().clone()),
|
||||
.with_leverage(order.order_form().leverage())
|
||||
.with_metadata(order.order_form().metadata().clone()),
|
||||
})
|
||||
}
|
||||
|
||||
|
89
src/tests.rs
89
src/tests.rs
@ -1,89 +0,0 @@
|
||||
#[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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user