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.
|
# It is not intended for manual editing.
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.14.1"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
@ -75,9 +75,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.56"
|
version = "0.3.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
|
checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
@ -109,7 +109,7 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tungstenite 0.13.0",
|
"tungstenite",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -237,17 +237,6 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
|
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ears"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e741c53078ec208f07c837fca26cd2624f959140c52268ee0677ad3b8e9b2112"
|
|
||||||
dependencies = [
|
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.26"
|
version = "0.8.26"
|
||||||
@ -802,9 +791,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.23.0"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
|
checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
@ -1170,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
|
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustico"
|
name = "rustybot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1179,7 +1168,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ears",
|
|
||||||
"fern",
|
"fern",
|
||||||
"float-cmp",
|
"float-cmp",
|
||||||
"futures-retry",
|
"futures-retry",
|
||||||
@ -1189,7 +1177,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"tungstenite 0.12.0",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1353,26 +1341,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1470,7 +1438,7 @@ dependencies = [
|
|||||||
"log 0.4.11",
|
"log 0.4.11",
|
||||||
"pin-project 1.0.2",
|
"pin-project 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tungstenite 0.12.0",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1551,27 +1519,6 @@ dependencies = [
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tungstenite"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"byteorder",
|
|
||||||
"bytes 1.0.1",
|
|
||||||
"http",
|
|
||||||
"httparse",
|
|
||||||
"input_buffer",
|
|
||||||
"log 0.4.11",
|
|
||||||
"native-tls",
|
|
||||||
"rand 0.8.2",
|
|
||||||
"sha-1",
|
|
||||||
"thiserror",
|
|
||||||
"url",
|
|
||||||
"utf-8",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustico"
|
name = "rustybot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Giulio De Pasquale <depasquale@giugl.io>"]
|
authors = ["Giulio De Pasquale <depasquale@giugl.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@ -23,4 +23,3 @@ futures-retry = "0.6"
|
|||||||
tungstenite = "0.12"
|
tungstenite = "0.12"
|
||||||
tokio-tungstenite = "0.13"
|
tokio-tungstenite = "0.13"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
ears = "0.8.0"
|
|
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 log::{error, info};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
use crate::connectors::ExchangeDetails;
|
use crate::connectors::ExchangeDetails;
|
||||||
use crate::currency::{Symbol, SymbolPair};
|
use crate::currency::{Symbol, SymbolPair};
|
||||||
use crate::frontend::FrontendManagerHandle;
|
use crate::frontend::FrontendManagerHandle;
|
||||||
use crate::managers::ExchangeManager;
|
use crate::managers::ExchangeManager;
|
||||||
use crate::ticker::Ticker;
|
use crate::ticker::Ticker;
|
||||||
|
use crate::BoxError;
|
||||||
|
|
||||||
pub struct Rustico {
|
pub struct BfxBot {
|
||||||
ticker: Ticker,
|
ticker: Ticker,
|
||||||
exchange_managers: Vec<ExchangeManager>,
|
exchange_managers: Vec<ExchangeManager>,
|
||||||
frontend_connector: FrontendManagerHandle,
|
frontend_connector: FrontendManagerHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rustico {
|
impl BfxBot {
|
||||||
// TODO: change constructor to take SymbolPairs and not Symbol
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
exchanges: Vec<ExchangeDetails>,
|
exchanges: Vec<ExchangeDetails>,
|
||||||
trading_pairs: Vec<SymbolPair>,
|
trading_symbols: Vec<Symbol>,
|
||||||
|
quote: Symbol,
|
||||||
tick_duration: Duration,
|
tick_duration: Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let exchange_managers = exchanges
|
let pairs: Vec<_> = trading_symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| ExchangeManager::new(x, &trading_pairs))
|
.map(|x| SymbolPair::new(quote.clone(), x.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Rustico {
|
let exchange_managers = exchanges
|
||||||
|
.iter()
|
||||||
|
.map(|x| ExchangeManager::new(x, &pairs))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
BfxBot {
|
||||||
ticker: Ticker::new(tick_duration),
|
ticker: Ticker::new(tick_duration),
|
||||||
exchange_managers,
|
exchange_managers,
|
||||||
frontend_connector: FrontendManagerHandle::new(),
|
frontend_connector: FrontendManagerHandle::new(),
|
||||||
|
@ -4,23 +4,22 @@ use std::str::FromStr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bitfinex::api::RestClient;
|
use bitfinex::api::Bitfinex;
|
||||||
use bitfinex::book::BookPrecision;
|
use bitfinex::book::BookPrecision;
|
||||||
use bitfinex::orders::{CancelOrderForm, OrderMeta};
|
use bitfinex::orders::{CancelOrderForm, OrderMeta};
|
||||||
use bitfinex::responses::{OrderResponse, TradeResponse};
|
use bitfinex::responses::{OrderResponse, TradeResponse};
|
||||||
use bitfinex::ticker::TradingPairTicker;
|
use bitfinex::ticker::TradingPairTicker;
|
||||||
use bitfinex::websockets::WebSocketClient;
|
|
||||||
use futures_retry::RetryPolicy;
|
use futures_retry::RetryPolicy;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use tokio::macros::support::Future;
|
use tokio::macros::support::Future;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
use crate::currency::{Symbol, SymbolPair};
|
use crate::currency::{Symbol, SymbolPair};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
|
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
|
||||||
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
|
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
|
||||||
};
|
};
|
||||||
|
use crate::BoxError;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||||
pub enum Exchange {
|
pub enum Exchange {
|
||||||
@ -37,7 +36,7 @@ pub enum ExchangeDetails {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
inner: Arc<Box<dyn RestConnector>>,
|
inner: Arc<Box<dyn Connector>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
@ -124,7 +123,7 @@ impl Client {
|
|||||||
.active_orders(pair)
|
.active_orders(pair)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|x| x.pair() == pair)
|
.filter(|x| &x.pair() == &pair)
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,13 +164,10 @@ impl Client {
|
|||||||
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
||||||
self.inner.orders_history(pair).await
|
self.inner.orders_history(pair).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { self.inner.trading_fees().await }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait represents a REST API service.
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait RestConnector: Send + Sync {
|
pub trait Connector: Send + Sync {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError>;
|
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError>;
|
||||||
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError>;
|
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError>;
|
||||||
@ -195,26 +191,18 @@ pub trait RestConnector: Send + Sync {
|
|||||||
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>;
|
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 {
|
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(f, "{}", self.name())
|
write!(f, "{}", self.name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait represents a WebSocket API service.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WebSocketConnector: Send + Sync {
|
|
||||||
fn name(&self) -> String;
|
|
||||||
async fn connect(&self) -> Result<(), BoxError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************
|
/**************
|
||||||
* BITFINEX
|
* BITFINEX
|
||||||
**************/
|
**************/
|
||||||
|
|
||||||
pub struct BitfinexConnector {
|
pub struct BitfinexConnector {
|
||||||
rest: bitfinex::api::RestClient,
|
bfx: Bitfinex,
|
||||||
ws: bitfinex::websockets::WebSocketClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitfinexConnector {
|
impl BitfinexConnector {
|
||||||
@ -229,8 +217,7 @@ impl BitfinexConnector {
|
|||||||
|
|
||||||
pub fn new(api_key: &str, api_secret: &str) -> Self {
|
pub fn new(api_key: &str, api_secret: &str) -> Self {
|
||||||
BitfinexConnector {
|
BitfinexConnector {
|
||||||
rest: RestClient::new(Some(api_key.into()), Some(api_secret.into())),
|
bfx: Bitfinex::new(Some(api_key.into()), Some(api_secret.into())),
|
||||||
ws: WebSocketClient::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +237,7 @@ impl BitfinexConnector {
|
|||||||
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
|
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
|
||||||
where
|
where
|
||||||
F: FnMut() -> Fut,
|
F: FnMut() -> Fut,
|
||||||
Fut: Future<Output=Result<O, BoxError>>,
|
Fut: Future<Output = Result<O, BoxError>>,
|
||||||
{
|
{
|
||||||
let response = {
|
let response = {
|
||||||
loop {
|
loop {
|
||||||
@ -271,14 +258,14 @@ impl BitfinexConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl RestConnector for BitfinexConnector {
|
impl Connector for BitfinexConnector {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"Bitfinex REST".into()
|
"Bitfinex".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
|
async fn active_positions(&self, pair: &SymbolPair) -> Result<Option<Vec<Position>>, BoxError> {
|
||||||
let active_positions =
|
let active_positions =
|
||||||
BitfinexConnector::retry_nonce(|| self.rest.positions.active_positions()).await?;
|
BitfinexConnector::retry_nonce(|| self.bfx.positions.active_positions()).await?;
|
||||||
|
|
||||||
let positions: Vec<_> = active_positions
|
let positions: Vec<_> = active_positions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -286,14 +273,14 @@ impl RestConnector for BitfinexConnector {
|
|||||||
.filter(|x: &Position| x.pair() == pair)
|
.filter(|x: &Position| x.pair() == pair)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
trace!("\tRetrieved positions for {}", pair);
|
trace!("\t[PositionManager] Retrieved positions for {}", pair);
|
||||||
Ok((!positions.is_empty()).then_some(positions))
|
Ok((!positions.is_empty()).then_some(positions))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
|
async fn current_prices(&self, pair: &SymbolPair) -> Result<TradingPairTicker, BoxError> {
|
||||||
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
||||||
|
|
||||||
let ticker: TradingPairTicker = self.rest.ticker.trading_pair(symbol_name).await?;
|
let ticker: TradingPairTicker = self.bfx.ticker.trading_pair(symbol_name).await?;
|
||||||
|
|
||||||
Ok(ticker)
|
Ok(ticker)
|
||||||
}
|
}
|
||||||
@ -302,7 +289,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
let symbol_name = BitfinexConnector::format_trading_pair(pair);
|
||||||
|
|
||||||
let response = BitfinexConnector::retry_nonce(|| {
|
let response = BitfinexConnector::retry_nonce(|| {
|
||||||
self.rest.book.trading_pair(&symbol_name, BookPrecision::P0)
|
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -319,7 +306,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
|
async fn active_orders(&self, _: &SymbolPair) -> Result<Vec<ActiveOrder>, BoxError> {
|
||||||
let response = BitfinexConnector::retry_nonce(|| self.rest.orders.active_orders()).await?;
|
let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.active_orders()).await?;
|
||||||
|
|
||||||
Ok(response.iter().map(Into::into).collect())
|
Ok(response.iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
@ -329,6 +316,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
let amount = order.amount();
|
let amount = order.amount();
|
||||||
|
|
||||||
let order_form = {
|
let order_form = {
|
||||||
|
let pre_leverage = {
|
||||||
match order.kind() {
|
match order.kind() {
|
||||||
OrderKind::Limit { price } => {
|
OrderKind::Limit { price } => {
|
||||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||||
@ -339,13 +327,13 @@ impl RestConnector for BitfinexConnector {
|
|||||||
OrderKind::Stop { price } => {
|
OrderKind::Stop { price } => {
|
||||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||||
}
|
}
|
||||||
OrderKind::StopLimit { stop_price: price, limit_price } => {
|
OrderKind::StopLimit { price, limit_price } => {
|
||||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||||
.with_price_aux_limit(Some(limit_price))?
|
.with_price_aux_limit(limit_price)?
|
||||||
}
|
}
|
||||||
OrderKind::TrailingStop { distance } => {
|
OrderKind::TrailingStop { distance } => {
|
||||||
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into())
|
||||||
.with_price_trailing(Some(distance))?
|
.with_price_trailing(distance)?
|
||||||
}
|
}
|
||||||
OrderKind::FillOrKill { price } => {
|
OrderKind::FillOrKill { price } => {
|
||||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||||
@ -354,28 +342,31 @@ impl RestConnector for BitfinexConnector {
|
|||||||
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.with_meta(Some(OrderMeta::new(
|
.with_meta(OrderMeta::new(
|
||||||
BitfinexConnector::AFFILIATE_CODE.to_string(),
|
BitfinexConnector::AFFILIATE_CODE.to_string(),
|
||||||
)))
|
))
|
||||||
// TODO: CHANGEME!
|
};
|
||||||
.with_leverage(Some(15))
|
|
||||||
|
// adding leverage, if any
|
||||||
|
match order.leverage() {
|
||||||
|
// TODO: CHANGEME!!!!
|
||||||
|
Some(_leverage) => pre_leverage.with_leverage(15),
|
||||||
|
// Some(leverage) => pre_leverage.with_leverage(leverage.round() as u32),
|
||||||
|
None => pre_leverage,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let response =
|
let response =
|
||||||
BitfinexConnector::retry_nonce(|| self.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
|
Ok((&response).try_into()?)
|
||||||
let order_response: ActiveOrder = (&response).try_into()?;
|
|
||||||
|
|
||||||
// TODO: CHANGEME!!!!
|
|
||||||
Ok(order_response.with_leverage(Some(15.0)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
|
async fn cancel_order(&self, order: &ActiveOrder) -> Result<ActiveOrder, BoxError> {
|
||||||
let cancel_form = order.into();
|
let cancel_form = order.into();
|
||||||
|
|
||||||
let response =
|
let response =
|
||||||
BitfinexConnector::retry_nonce(|| self.rest.orders.cancel_order(&cancel_form)).await?;
|
BitfinexConnector::retry_nonce(|| self.bfx.orders.cancel_order(&cancel_form)).await?;
|
||||||
|
|
||||||
Ok((&response).try_into()?)
|
Ok((&response).try_into()?)
|
||||||
}
|
}
|
||||||
@ -388,7 +379,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
amount: f64,
|
amount: f64,
|
||||||
) -> Result<(), BoxError> {
|
) -> Result<(), BoxError> {
|
||||||
BitfinexConnector::retry_nonce(|| {
|
BitfinexConnector::retry_nonce(|| {
|
||||||
self.rest.account.transfer_between_wallets(
|
self.bfx.account.transfer_between_wallets(
|
||||||
from.into(),
|
from.into(),
|
||||||
to.into(),
|
to.into(),
|
||||||
symbol.to_string(),
|
symbol.to_string(),
|
||||||
@ -405,7 +396,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
order: &OrderDetails,
|
order: &OrderDetails,
|
||||||
) -> Result<Option<Vec<Trade>>, BoxError> {
|
) -> Result<Option<Vec<Trade>>, BoxError> {
|
||||||
let response = BitfinexConnector::retry_nonce(|| {
|
let response = BitfinexConnector::retry_nonce(|| {
|
||||||
self.rest
|
self.bfx
|
||||||
.trades
|
.trades
|
||||||
.generated_by_order(order.pair().trading_repr(), order.id())
|
.generated_by_order(order.pair().trading_repr(), order.id())
|
||||||
})
|
})
|
||||||
@ -423,7 +414,7 @@ impl RestConnector for BitfinexConnector {
|
|||||||
pair: &SymbolPair,
|
pair: &SymbolPair,
|
||||||
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
|
||||||
let response =
|
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?;
|
.await?;
|
||||||
let mapped_vec: Vec<_> = response.iter().map(Into::into).collect();
|
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> {
|
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> {
|
||||||
let mut fees = vec![];
|
let mut fees = vec![];
|
||||||
let accountfees =
|
let accountfees =
|
||||||
BitfinexConnector::retry_nonce(|| self.rest.account.account_summary()).await?;
|
BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?;
|
||||||
|
|
||||||
// Derivatives
|
// Derivatives
|
||||||
let derivative_taker = TradingFees::Taker {
|
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 {
|
impl From<&ActiveOrder> for CancelOrderForm {
|
||||||
fn from(o: &ActiveOrder) -> Self {
|
fn from(o: &ActiveOrder) -> Self {
|
||||||
Self::from_id(o.id())
|
Self::from_id(o.id())
|
||||||
@ -531,6 +511,7 @@ impl TryInto<Position> for bitfinex::positions::Position {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
println!("leverage: {}", self.leverage());
|
||||||
Ok(Position::new(
|
Ok(Position::new(
|
||||||
SymbolPair::from_str(self.symbol())?,
|
SymbolPair::from_str(self.symbol())?,
|
||||||
state,
|
state,
|
||||||
@ -622,7 +603,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
|
|||||||
}
|
}
|
||||||
bitfinex::orders::OrderKind::StopLimit
|
bitfinex::orders::OrderKind::StopLimit
|
||||||
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
||||||
stop_price: response.price(),
|
price: response.price(),
|
||||||
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
||||||
},
|
},
|
||||||
bitfinex::orders::OrderKind::TrailingStop
|
bitfinex::orders::OrderKind::TrailingStop
|
||||||
@ -661,7 +642,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
|
|||||||
}
|
}
|
||||||
bitfinex::orders::OrderKind::StopLimit
|
bitfinex::orders::OrderKind::StopLimit
|
||||||
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
|
||||||
stop_price: response.price(),
|
price: response.price(),
|
||||||
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
limit_price: response.price_aux_limit().expect("Limit price not found!"),
|
||||||
},
|
},
|
||||||
bitfinex::orders::OrderKind::TrailingStop
|
bitfinex::orders::OrderKind::TrailingStop
|
||||||
|
@ -13,7 +13,7 @@ pub struct Symbol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<S> From<S> for Symbol
|
impl<S> From<S> for Symbol
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
{
|
{
|
||||||
fn from(item: S) -> Self {
|
fn from(item: S) -> Self {
|
||||||
@ -31,8 +31,6 @@ impl Symbol {
|
|||||||
pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0");
|
pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0");
|
||||||
pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0");
|
pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0");
|
||||||
pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0");
|
pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0");
|
||||||
pub const DERIV_ADA: Symbol = Symbol::new_static("ADAF0");
|
|
||||||
pub const DERIV_POLKADOT: Symbol = Symbol::new_static("DOTF0");
|
|
||||||
|
|
||||||
// Paper trading
|
// Paper trading
|
||||||
pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC");
|
pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC");
|
||||||
@ -76,8 +74,8 @@ pub struct SymbolPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SymbolPair {
|
impl SymbolPair {
|
||||||
pub fn new(base: Symbol, quote: Symbol) -> Self {
|
pub fn new(quote: Symbol, base: Symbol) -> Self {
|
||||||
SymbolPair { base, quote }
|
SymbolPair { quote, base }
|
||||||
}
|
}
|
||||||
pub fn trading_repr(&self) -> String {
|
pub fn trading_repr(&self) -> String {
|
||||||
format!("t{}{}", self.base, self.quote)
|
format!("t{}{}", self.base, self.quote)
|
||||||
|
@ -56,19 +56,14 @@ pub struct Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
pub fn new(kind: EventKind, tick: u64) -> Self {
|
pub fn new(kind: EventKind, tick: u64, metadata: Option<EventMetadata>) -> Self {
|
||||||
Event {
|
Event {
|
||||||
kind,
|
kind,
|
||||||
tick,
|
tick,
|
||||||
metadata: None,
|
metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_metadata(mut self, metadata: Option<EventMetadata>) -> Self {
|
|
||||||
self.metadata = metadata;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_metadata(&self) -> bool {
|
fn has_metadata(&self) -> bool {
|
||||||
self.metadata.is_some()
|
self.metadata.is_some()
|
||||||
}
|
}
|
||||||
|
@ -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::stream::TryStreamExt;
|
||||||
use futures_util::StreamExt;
|
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 std::net::SocketAddr;
|
||||||
use crate::events::ActorMessage;
|
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio_tungstenite::accept_async;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FrontendManager {
|
pub struct FrontendManager {
|
||||||
|
51
src/main.rs
51
src/main.rs
@ -4,13 +4,12 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use fern::colors::{Color, ColoredLevelConfig};
|
use fern::colors::{Color, ColoredLevelConfig};
|
||||||
use log::error;
|
use log::LevelFilter::{Trace};
|
||||||
use log::LevelFilter::Info;
|
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
use crate::bot::Rustico;
|
use crate::bot::BfxBot;
|
||||||
use crate::connectors::ExchangeDetails;
|
use crate::connectors::ExchangeDetails;
|
||||||
use crate::currency::{Symbol, SymbolPair};
|
use crate::currency::Symbol;
|
||||||
|
|
||||||
mod bot;
|
mod bot;
|
||||||
mod connectors;
|
mod connectors;
|
||||||
@ -21,22 +20,13 @@ mod managers;
|
|||||||
mod models;
|
mod models;
|
||||||
mod strategy;
|
mod strategy;
|
||||||
mod ticker;
|
mod ticker;
|
||||||
mod tests;
|
|
||||||
mod sounds;
|
|
||||||
|
|
||||||
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), BoxError> {
|
async fn main() -> Result<(), BoxError> {
|
||||||
if let Err(e) = setup_logger() {
|
setup_logger()?;
|
||||||
error!("Could not setup logger: {}", e);
|
dotenv::dotenv()?;
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = dotenv::dotenv() {
|
|
||||||
error!("Could not open .env file: {}", e);
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let api_key = env::vars()
|
let api_key = env::vars()
|
||||||
.find(|(k, _v)| k == "API_KEY")
|
.find(|(k, _v)| k == "API_KEY")
|
||||||
@ -48,24 +38,14 @@ async fn main() -> Result<(), BoxError> {
|
|||||||
.ok_or("API_SECRET not set!")?;
|
.ok_or("API_SECRET not set!")?;
|
||||||
|
|
||||||
let bitfinex = ExchangeDetails::Bitfinex {
|
let bitfinex = ExchangeDetails::Bitfinex {
|
||||||
api_key,
|
api_key: api_key.into(),
|
||||||
api_secret,
|
api_secret: api_secret.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pairs = vec![
|
let mut bot = BfxBot::new(
|
||||||
SymbolPair::new(Symbol::BTC, Symbol::USD),
|
|
||||||
SymbolPair::new(Symbol::XMR, Symbol::USD),
|
|
||||||
SymbolPair::new(Symbol::ETH, Symbol::USD),
|
|
||||||
SymbolPair::new(Symbol::DERIV_ADA, Symbol::DERIV_USDT),
|
|
||||||
SymbolPair::new(Symbol::DERIV_POLKADOT, Symbol::DERIV_USDT),
|
|
||||||
SymbolPair::new(Symbol::DERIV_BTC, Symbol::DERIV_USDT),
|
|
||||||
SymbolPair::new(Symbol::DERIV_ETH, Symbol::DERIV_USDT),
|
|
||||||
SymbolPair::new(Symbol::DERIV_TESTBTC, Symbol::DERIV_TESTUSDT),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut bot = Rustico::new(
|
|
||||||
vec![bitfinex],
|
vec![bitfinex],
|
||||||
pairs,
|
vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC],
|
||||||
|
Symbol::DERIV_USDT,
|
||||||
Duration::new(10, 0),
|
Duration::new(10, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -83,17 +63,16 @@ fn setup_logger() -> Result<(), fern::InitError> {
|
|||||||
fern::Dispatch::new()
|
fern::Dispatch::new()
|
||||||
.format(move |out, message, record| {
|
.format(move |out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
"{} | [{}][{}] | {}",
|
"[{}][{}] {}",
|
||||||
chrono::Local::now().format("[%d/%m/%Y][%H:%M:%S]"),
|
record.target(),
|
||||||
record.target().strip_prefix("rustico::").unwrap_or("rustico"),
|
|
||||||
colors.color(record.level()),
|
colors.color(record.level()),
|
||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(Info)
|
.level(Trace)
|
||||||
.filter(|metadata| metadata.target().contains("rustico"))
|
.filter(|metadata| metadata.target().contains("rustybot"))
|
||||||
.chain(std::io::stdout())
|
.chain(std::io::stdout())
|
||||||
.chain(fern::log_file("rustico.log")?)
|
// .chain(fern::log_file("rustico.log")?)
|
||||||
.apply()?;
|
.apply()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
258
src/managers.rs
258
src/managers.rs
@ -1,22 +1,23 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::ops::Neg;
|
use std::ops::Neg;
|
||||||
|
|
||||||
use futures_util::stream::FuturesUnordered;
|
use futures_util::stream::FuturesUnordered;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
use merge::Merge;
|
use merge::Merge;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
|
||||||
use tokio::sync::mpsc::channel;
|
use tokio::sync::mpsc::channel;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
use crate::connectors::{Client, ExchangeDetails};
|
use crate::connectors::{Client, ExchangeDetails};
|
||||||
use crate::currency::SymbolPair;
|
use crate::currency::SymbolPair;
|
||||||
use crate::events::{ActionMessage, ActorMessage, Event};
|
use crate::events::{ActionMessage, ActorMessage, Event};
|
||||||
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PriceTicker};
|
use crate::models::{
|
||||||
use crate::sounds::{MARKET_ORDER_PLACED_PATH, play_sound};
|
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker,
|
||||||
use crate::strategy::{MarketEnforce, PositionStrategy, TrailingStop};
|
};
|
||||||
|
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
|
||||||
|
use crate::BoxError;
|
||||||
|
|
||||||
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
||||||
|
|
||||||
@ -233,9 +234,8 @@ impl PositionManager {
|
|||||||
pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> {
|
pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> {
|
||||||
trace!("\t[PositionManager] Updating {}", self.pair);
|
trace!("\t[PositionManager] Updating {}", self.pair);
|
||||||
|
|
||||||
|
let opt_active_positions = self.client.active_positions(&self.pair).await?;
|
||||||
self.current_tick = tick;
|
self.current_tick = tick;
|
||||||
let (fees, opt_active_positions) = tokio::join!(self.client.trading_fees(),self.client.active_positions(&self.pair));
|
|
||||||
let (fees, opt_active_positions) = (fees?, opt_active_positions?);
|
|
||||||
|
|
||||||
// we assume there is only ONE active position per pair
|
// we assume there is only ONE active position per pair
|
||||||
match opt_active_positions {
|
match opt_active_positions {
|
||||||
@ -258,11 +258,11 @@ impl PositionManager {
|
|||||||
|
|
||||||
let (pos_on_tick, events_on_tick, messages_on_tick) = self
|
let (pos_on_tick, events_on_tick, messages_on_tick) = self
|
||||||
.strategy
|
.strategy
|
||||||
.on_tick(position, self.current_tick(), &self.positions_history, &fees);
|
.on_tick(position, self.current_tick(), &self.positions_history);
|
||||||
|
|
||||||
let (pos_post_tick, events_post_tick, messages_post_tick) = self
|
let (pos_post_tick, events_post_tick, messages_post_tick) = self
|
||||||
.strategy
|
.strategy
|
||||||
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history, &fees);
|
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history);
|
||||||
|
|
||||||
events.merge(events_on_tick);
|
events.merge(events_on_tick);
|
||||||
events.merge(events_post_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
|
* ORDERS
|
||||||
******************/
|
******************/
|
||||||
|
|
||||||
|
// Position ID: Order ID
|
||||||
|
pub type TrackedPositionsMap = HashMap<u64, Vec<u64>>;
|
||||||
|
|
||||||
pub struct OrderManagerHandle {
|
pub struct OrderManagerHandle {
|
||||||
sender: Sender<ActorMessage>,
|
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 (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));
|
tokio::spawn(OrderManagerHandle::run_order_manager(manager));
|
||||||
|
|
||||||
@ -366,9 +384,11 @@ impl OrderManagerHandle {
|
|||||||
|
|
||||||
pub struct OrderManager {
|
pub struct OrderManager {
|
||||||
receiver: Receiver<ActorMessage>,
|
receiver: Receiver<ActorMessage>,
|
||||||
orders_map: HashMap<u64, HashSet<ActiveOrder>>,
|
tracked_positions: TrackedPositionsMap,
|
||||||
pair: SymbolPair,
|
pair: SymbolPair,
|
||||||
|
open_orders: Vec<ActiveOrder>,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
strategy: Box<dyn OrderStrategy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OrderManager {
|
impl OrderManager {
|
||||||
@ -376,93 +396,18 @@ impl OrderManager {
|
|||||||
receiver: Receiver<ActorMessage>,
|
receiver: Receiver<ActorMessage>,
|
||||||
pair: SymbolPair,
|
pair: SymbolPair,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
strategy: Box<dyn OrderStrategy>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
OrderManager {
|
OrderManager {
|
||||||
receiver,
|
receiver,
|
||||||
pair,
|
pair,
|
||||||
|
open_orders: Vec::new(),
|
||||||
client,
|
client,
|
||||||
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> {
|
pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> {
|
||||||
let (events, messages) = match msg.message {
|
let (events, messages) = match msg.message {
|
||||||
ActionMessage::Update { .. } => self.update().await?,
|
ActionMessage::Update { .. } => self.update().await?,
|
||||||
@ -484,7 +429,14 @@ impl OrderManager {
|
|||||||
pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> {
|
pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> {
|
||||||
info!("Closing outstanding orders for position #{}", position_id);
|
info!("Closing outstanding orders for position #{}", position_id);
|
||||||
|
|
||||||
if let Some(position_orders) = self.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 {
|
for order in position_orders {
|
||||||
match self.client.cancel_order(order).await {
|
match self.client.cancel_order(order).await {
|
||||||
Ok(_) => info!("Order #{} closed successfully.", order.id()),
|
Ok(_) => info!("Order #{} closed successfully.", order.id()),
|
||||||
@ -498,33 +450,25 @@ impl OrderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result<OptionUpdate, BoxError> {
|
pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result<OptionUpdate, BoxError> {
|
||||||
info!("Submitting order: {}", order_form.kind());
|
info!("Submiting {}", order_form.kind());
|
||||||
|
|
||||||
// adding strategy to order, if present in the metadata
|
let active_order = self.client.submit_order(order_form).await?;
|
||||||
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?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
debug!("Adding order to tracked orders.");
|
||||||
if let Some(metadata) = order_form.metadata() {
|
if let Some(metadata) = order_form.metadata() {
|
||||||
if let Some(position_id) = metadata.position_id() {
|
if let Some(position_id) = metadata.position_id() {
|
||||||
debug!("Adding order to tracked orders.");
|
match self.tracked_positions.get_mut(&position_id) {
|
||||||
|
None => {
|
||||||
if !self.add_to_orders_map(position_id, active_order) {
|
self.tracked_positions
|
||||||
error!("Failed while adding order to internal mapping.");
|
.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!
|
// TODO: return valid messages and events!111!!!1!
|
||||||
Ok((None, None))
|
Ok((None, None))
|
||||||
}
|
}
|
||||||
@ -569,14 +513,10 @@ impl OrderManager {
|
|||||||
position.platform(),
|
position.platform(),
|
||||||
position.amount().neg(),
|
position.amount().neg(),
|
||||||
)
|
)
|
||||||
.with_leverage(Some(position.leverage()))
|
.with_leverage(Some(position.leverage()));
|
||||||
.with_metadata(Some(OrderMetadata::new()
|
|
||||||
.with_strategy(Some(Box::new(MarketEnforce::default())))
|
|
||||||
.with_position_id(Some(position.id())))
|
|
||||||
);
|
|
||||||
|
|
||||||
// submitting order
|
info!("Submitting {} order", order_form.kind());
|
||||||
if let Err(e) = self.submit_order(&order_form).await {
|
if let Err(e) = self.client.submit_order(&order_form).await {
|
||||||
error!(
|
error!(
|
||||||
"Could not submit {} to close position #{}: {}",
|
"Could not submit {} to close position #{}: {}",
|
||||||
order_form.kind(),
|
order_form.kind(),
|
||||||
@ -595,26 +535,61 @@ impl OrderManager {
|
|||||||
pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
|
pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
|
||||||
debug!("\t[OrderManager] Updating {}", self.pair);
|
debug!("\t[OrderManager] Updating {}", self.pair);
|
||||||
|
|
||||||
// updating internal orders' mapping from remote
|
let (res_open_orders, res_order_book) = tokio::join!(
|
||||||
self.update_orders_map_from_remote().await?;
|
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();
|
|
||||||
|
|
||||||
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?;
|
|
||||||
|
|
||||||
for active_order in tracked_orders.iter().filter(|x| x.strategy().is_some()) {
|
|
||||||
let strategy = active_order.strategy().as_ref().unwrap();
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
"Found open order with \"{}\" strategy.",
|
|
||||||
strategy.name()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// executing the order's strategy and collecting its messages, if any
|
let (open_orders, order_book) = (res_open_orders?, res_order_book?);
|
||||||
let (_, strat_messages) = strategy.on_open_order(&active_order, &order_book)?;
|
|
||||||
|
// retrieving open positions to check whether the positions have open orders.
|
||||||
|
// we need to update our internal mapping in that case.
|
||||||
|
if !open_orders.is_empty() {
|
||||||
|
let open_positions = self.client.active_positions(&self.pair).await?;
|
||||||
|
|
||||||
|
if let Some(positions) = open_positions {
|
||||||
|
// currently, we are only trying to match orders with an amount equal to
|
||||||
|
// a position amount.
|
||||||
|
for position in positions {
|
||||||
|
let matching_order = open_orders
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.order_form().amount().abs() == position.amount().abs());
|
||||||
|
|
||||||
|
// if an order is found, we insert the order to our internal mapping, if not already present
|
||||||
|
if let Some(matching_order) = matching_order {
|
||||||
|
match self.tracked_positions.get_mut(&position.id()) {
|
||||||
|
Some(position_orders) => {
|
||||||
|
if !position_orders.contains(&matching_order.id()) {
|
||||||
|
trace!(
|
||||||
|
"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()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, strat_messages) = self.strategy.on_open_order(&active_order, &order_book)?;
|
||||||
|
|
||||||
if let Some(messages) = strat_messages {
|
if let Some(messages) = strat_messages {
|
||||||
for m in messages {
|
for m in messages {
|
||||||
@ -625,7 +600,7 @@ impl OrderManager {
|
|||||||
self.client.cancel_order(&active_order).await?;
|
self.client.cancel_order(&active_order).await?;
|
||||||
|
|
||||||
info!("\tSubmitting {}...", order_form.kind());
|
info!("\tSubmitting {}...", order_form.kind());
|
||||||
self.submit_order(&order_form).await?;
|
self.client.submit_order(&order_form).await?;
|
||||||
info!("Done!");
|
info!("Done!");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@ -637,12 +612,10 @@ impl OrderManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok((None, None))
|
Ok((None, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn best_closing_price(&self, position: &Position, order_book: &OrderBook) -> f64 {
|
pub fn best_closing_price(&self, position: &Position, order_book: &OrderBook) -> f64 {
|
||||||
let ask = order_book.lowest_ask();
|
let ask = order_book.lowest_ask();
|
||||||
let bid = order_book.highest_bid();
|
let bid = order_book.highest_bid();
|
||||||
@ -684,11 +657,12 @@ impl PairManager {
|
|||||||
order_manager: OrderManagerHandle::new(
|
order_manager: OrderManagerHandle::new(
|
||||||
pair.clone(),
|
pair.clone(),
|
||||||
client.clone(),
|
client.clone(),
|
||||||
|
Box::new(MarketEnforce::default()),
|
||||||
),
|
),
|
||||||
position_manager: PositionManagerHandle::new(
|
position_manager: PositionManagerHandle::new(
|
||||||
pair,
|
pair,
|
||||||
client,
|
client,
|
||||||
Box::new(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::fmt::{Display, Formatter};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use dyn_clone::clone_box;
|
|
||||||
|
|
||||||
use crate::connectors::Exchange;
|
use crate::connectors::Exchange;
|
||||||
use crate::currency::{Symbol, SymbolPair};
|
use crate::currency::{Symbol, SymbolPair};
|
||||||
use crate::strategy::OrderStrategy;
|
|
||||||
|
|
||||||
/***************
|
/***************
|
||||||
* Prices
|
* Prices
|
||||||
@ -151,7 +148,7 @@ impl OrderDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ActiveOrder {
|
pub struct ActiveOrder {
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
id: u64,
|
id: u64,
|
||||||
@ -161,7 +158,6 @@ pub struct ActiveOrder {
|
|||||||
order_form: OrderForm,
|
order_form: OrderForm,
|
||||||
creation_timestamp: u64,
|
creation_timestamp: u64,
|
||||||
update_timestamp: u64,
|
update_timestamp: u64,
|
||||||
strategy: Option<Box<dyn OrderStrategy>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveOrder {
|
impl ActiveOrder {
|
||||||
@ -182,7 +178,6 @@ impl ActiveOrder {
|
|||||||
order_form,
|
order_form,
|
||||||
creation_timestamp,
|
creation_timestamp,
|
||||||
update_timestamp,
|
update_timestamp,
|
||||||
strategy: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,16 +191,6 @@ impl ActiveOrder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_strategy(mut self, strategy: Option<Box<dyn OrderStrategy>>) -> Self {
|
|
||||||
self.strategy = strategy;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_leverage(mut self, leverage: Option<f64>) -> Self {
|
|
||||||
self.order_form = self.order_form.with_leverage(leverage);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exchange(&self) -> Exchange {
|
pub fn exchange(&self) -> Exchange {
|
||||||
self.exchange
|
self.exchange
|
||||||
}
|
}
|
||||||
@ -230,9 +215,6 @@ impl ActiveOrder {
|
|||||||
pub fn update_timestamp(&self) -> u64 {
|
pub fn update_timestamp(&self) -> u64 {
|
||||||
self.update_timestamp
|
self.update_timestamp
|
||||||
}
|
}
|
||||||
pub fn strategy(&self) -> &Option<Box<dyn OrderStrategy>> {
|
|
||||||
&self.strategy
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for ActiveOrder {
|
impl Hash for ActiveOrder {
|
||||||
@ -249,22 +231,6 @@ impl PartialEq for ActiveOrder {
|
|||||||
|
|
||||||
impl Eq for ActiveOrder {}
|
impl Eq for ActiveOrder {}
|
||||||
|
|
||||||
impl Clone for ActiveOrder {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
exchange: self.exchange,
|
|
||||||
id: self.id,
|
|
||||||
group_id: self.group_id,
|
|
||||||
client_id: self.client_id,
|
|
||||||
pair: self.pair.clone(),
|
|
||||||
order_form: self.order_form.clone(),
|
|
||||||
creation_timestamp: self.creation_timestamp,
|
|
||||||
update_timestamp: self.update_timestamp,
|
|
||||||
strategy: self.strategy.as_ref().map(|x| clone_box(&**x)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum TradingPlatform {
|
pub enum TradingPlatform {
|
||||||
Exchange,
|
Exchange,
|
||||||
@ -295,7 +261,7 @@ pub enum OrderKind {
|
|||||||
Limit { price: f64 },
|
Limit { price: f64 },
|
||||||
Market,
|
Market,
|
||||||
Stop { price: f64 },
|
Stop { price: f64 },
|
||||||
StopLimit { stop_price: f64, limit_price: f64 },
|
StopLimit { price: f64, limit_price: f64 },
|
||||||
TrailingStop { distance: f64 },
|
TrailingStop { distance: f64 },
|
||||||
FillOrKill { price: f64 },
|
FillOrKill { price: f64 },
|
||||||
ImmediateOrCancel { price: f64 },
|
ImmediateOrCancel { price: f64 },
|
||||||
@ -319,31 +285,31 @@ impl Display for OrderKind {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
OrderKind::Limit { price } => {
|
OrderKind::Limit { price } => {
|
||||||
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
|
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
|
||||||
}
|
}
|
||||||
OrderKind::Market => {
|
OrderKind::Market => {
|
||||||
write!(f, "[{}]", self.as_str())
|
write!(f, "[{}]", self.as_str())
|
||||||
}
|
}
|
||||||
OrderKind::Stop { price } => {
|
OrderKind::Stop { price } => {
|
||||||
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
|
write!(f, "[{} | Price: {:0.5}", self.as_str(), price,)
|
||||||
}
|
}
|
||||||
OrderKind::StopLimit { stop_price, limit_price } => {
|
OrderKind::StopLimit { price, limit_price } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"[{} | Stop: {:0.5}, Limit: {:0.5}]",
|
"[{} | Price: {:0.5}, Limit Price: {:0.5}]",
|
||||||
self.as_str(),
|
self.as_str(),
|
||||||
stop_price,
|
price,
|
||||||
limit_price
|
limit_price
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OrderKind::TrailingStop { distance } => {
|
OrderKind::TrailingStop { distance } => {
|
||||||
write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance, )
|
write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,)
|
||||||
}
|
}
|
||||||
OrderKind::FillOrKill { price } => {
|
OrderKind::FillOrKill { price } => {
|
||||||
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
|
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
|
||||||
}
|
}
|
||||||
OrderKind::ImmediateOrCancel { price } => {
|
OrderKind::ImmediateOrCancel { price } => {
|
||||||
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price, )
|
write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -389,90 +355,55 @@ impl OrderForm {
|
|||||||
pub fn pair(&self) -> &SymbolPair {
|
pub fn pair(&self) -> &SymbolPair {
|
||||||
&self.pair
|
&self.pair
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kind(&self) -> OrderKind {
|
pub fn kind(&self) -> OrderKind {
|
||||||
self.kind
|
self.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn platform(&self) -> &TradingPlatform {
|
pub fn platform(&self) -> &TradingPlatform {
|
||||||
&self.platform
|
&self.platform
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn amount(&self) -> f64 {
|
pub fn amount(&self) -> f64 {
|
||||||
self.amount
|
self.amount
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn price(&self) -> Option<f64> {
|
pub fn price(&self) -> Option<f64> {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
OrderKind::Limit { price, .. } => Some(price),
|
OrderKind::Limit { price, .. } => Some(price),
|
||||||
OrderKind::Market { .. } => None,
|
OrderKind::Market { .. } => None,
|
||||||
OrderKind::Stop { price, .. } => Some(price),
|
OrderKind::Stop { price, .. } => Some(price),
|
||||||
OrderKind::StopLimit { stop_price: price, .. } => Some(price),
|
OrderKind::StopLimit { price, .. } => Some(price),
|
||||||
OrderKind::TrailingStop { .. } => None,
|
OrderKind::TrailingStop { .. } => None,
|
||||||
OrderKind::FillOrKill { price, .. } => Some(price),
|
OrderKind::FillOrKill { price, .. } => Some(price),
|
||||||
OrderKind::ImmediateOrCancel { price, .. } => Some(price),
|
OrderKind::ImmediateOrCancel { price, .. } => Some(price),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leverage(&self) -> Option<f64> {
|
pub fn leverage(&self) -> Option<f64> {
|
||||||
self.leverage
|
self.leverage
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metadata(&self) -> &Option<OrderMetadata> {
|
pub fn metadata(&self) -> &Option<OrderMetadata> {
|
||||||
&self.metadata
|
&self.metadata
|
||||||
}
|
}
|
||||||
pub fn is_long(&self) -> bool {
|
|
||||||
self.amount.is_sign_positive()
|
|
||||||
}
|
|
||||||
pub fn is_short(&self) -> bool {
|
|
||||||
self.amount.is_sign_negative()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OrderMetadata {
|
pub struct OrderMetadata {
|
||||||
position_id: Option<u64>,
|
position_id: Option<u64>,
|
||||||
strategy: Option<Box<dyn OrderStrategy>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for OrderMetadata {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
position_id: self.position_id,
|
|
||||||
strategy: self.strategy.as_ref().map(|x| clone_box(&**x)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OrderMetadata {
|
impl OrderMetadata {
|
||||||
pub fn new() -> Self {
|
pub fn with_position_id(position_id: u64) -> Self {
|
||||||
Self {
|
OrderMetadata {
|
||||||
position_id: None,
|
position_id: Some(position_id),
|
||||||
strategy: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_position_id(mut self, position_id: Option<u64>) -> Self {
|
|
||||||
self.position_id = position_id;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_strategy(mut self, strategy: Option<Box<dyn OrderStrategy>>) -> Self {
|
|
||||||
self.strategy = strategy;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn position_id(&self) -> Option<u64> {
|
pub fn position_id(&self) -> Option<u64> {
|
||||||
self.position_id
|
self.position_id
|
||||||
}
|
}
|
||||||
pub fn cloned_strategy(&self) -> Option<Box<dyn OrderStrategy>> {
|
|
||||||
match &self.strategy {
|
|
||||||
None => { None }
|
|
||||||
Some(strategy) => {
|
|
||||||
Some(clone_box(&**strategy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OrderMetadata {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************
|
/***************
|
||||||
|
@ -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); }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
546
src/strategy.rs
546
src/strategy.rs
@ -1,16 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::ops::Neg;
|
|
||||||
|
|
||||||
use dyn_clone::DynClone;
|
use dyn_clone::DynClone;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::BoxError;
|
use crate::connectors::Connector;
|
||||||
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
|
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
|
||||||
use crate::managers::OptionUpdate;
|
use crate::managers::OptionUpdate;
|
||||||
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees};
|
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
|
||||||
use crate::models::PositionProfitState::{BreakEven, Critical, Loss, MinimumProfit, Profit};
|
use crate::BoxError;
|
||||||
use crate::sounds::{GOOD_PROFIT_SOUND_PATH, LOSS_TO_BREAK_EVEN_PATH, MIN_PROFIT_SOUND_PATH, play_sound};
|
|
||||||
|
|
||||||
/***************
|
/***************
|
||||||
* DEFINITIONS
|
* DEFINITIONS
|
||||||
@ -23,14 +21,12 @@ pub trait PositionStrategy: DynClone + Send + Sync {
|
|||||||
position: Position,
|
position: Position,
|
||||||
current_tick: u64,
|
current_tick: u64,
|
||||||
positions_history: &HashMap<u64, Position>,
|
positions_history: &HashMap<u64, Position>,
|
||||||
fees: &[TradingFees],
|
|
||||||
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
||||||
fn post_tick(
|
fn post_tick(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Position,
|
position: Position,
|
||||||
current_tick: u64,
|
current_tick: u64,
|
||||||
positions_history: &HashMap<u64, Position>,
|
positions_history: &HashMap<u64, Position>,
|
||||||
fees: &[TradingFees],
|
|
||||||
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,16 +67,8 @@ impl Debug for dyn OrderStrategy {
|
|||||||
***************/
|
***************/
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TrailingStop {
|
pub struct HiddenTrailingStop {
|
||||||
// Position ID: stop percentage mapping
|
|
||||||
stop_percentages: HashMap<u64, f64>,
|
stop_percentages: HashMap<u64, f64>,
|
||||||
// Position ID: bool mapping. Represents when the strategy has asked the
|
|
||||||
// order manager to set a stop loss order
|
|
||||||
stop_loss_flags: HashMap<u64, bool>,
|
|
||||||
// Position ID: bool mapping. Represents when the strategy has asked the
|
|
||||||
// order manager to set a limit order to close the position as the stop percentage
|
|
||||||
// has been surpassed
|
|
||||||
trail_set_flags: HashMap<u64, bool>,
|
|
||||||
capital_max_loss: f64,
|
capital_max_loss: f64,
|
||||||
capital_min_profit: f64,
|
capital_min_profit: f64,
|
||||||
capital_good_profit: f64,
|
capital_good_profit: f64,
|
||||||
@ -92,96 +80,55 @@ pub struct TrailingStop {
|
|||||||
max_loss_percentage: f64,
|
max_loss_percentage: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HiddenTrailingStop {
|
||||||
|
fn update_stop_percentage(&mut self, position: &Position) {
|
||||||
|
if let Some(profit_state) = position.profit_state() {
|
||||||
|
let profit_state_delta = match profit_state {
|
||||||
|
PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta),
|
||||||
|
PositionProfitState::Profit => Some(self.good_profit_trailing_delta),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
impl TrailingStop {
|
if let Some(profit_state_delta) = profit_state_delta {
|
||||||
fn play_sound_on_state(prev_position: &Position, current_position: &Position) {
|
let current_stop_percentage = position.pl_perc() - profit_state_delta;
|
||||||
if prev_position.profit_state().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_position.profit_state().is_none() {
|
if let PositionProfitState::MinimumProfit | PositionProfitState::Profit =
|
||||||
return;
|
profit_state
|
||||||
}
|
{
|
||||||
|
|
||||||
let prev_state = prev_position.profit_state().unwrap();
|
|
||||||
let current_state = current_position.profit_state().unwrap();
|
|
||||||
|
|
||||||
// negative to positive
|
|
||||||
if let Loss | Critical = prev_state {
|
|
||||||
match current_state {
|
|
||||||
PositionProfitState::BreakEven => { play_sound(LOSS_TO_BREAK_EVEN_PATH); }
|
|
||||||
PositionProfitState::MinimumProfit => { play_sound(MIN_PROFIT_SOUND_PATH); }
|
|
||||||
PositionProfitState::Profit => { play_sound(GOOD_PROFIT_SOUND_PATH); }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let BreakEven = prev_state {
|
|
||||||
match current_state {
|
|
||||||
PositionProfitState::MinimumProfit => { play_sound(MIN_PROFIT_SOUND_PATH); }
|
|
||||||
PositionProfitState::Profit => { play_sound(GOOD_PROFIT_SOUND_PATH); }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_status(&self, position: &Position) {
|
|
||||||
match self.stop_percentages.get(&position.id()) {
|
match self.stop_percentages.get(&position.id()) {
|
||||||
None => {
|
None => {
|
||||||
info!(
|
self.stop_percentages
|
||||||
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%)",
|
.insert(position.id(), current_stop_percentage);
|
||||||
position.profit_state().unwrap(),
|
|
||||||
position.pl(),
|
|
||||||
position.pair().quote(),
|
|
||||||
position.pl_perc()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(stop_percentage) => {
|
Some(existing_threshold) => {
|
||||||
|
if existing_threshold < ¤t_stop_percentage {
|
||||||
|
self.stop_percentages
|
||||||
|
.insert(position.id(), current_stop_percentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
|
"\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
|
||||||
position.profit_state().unwrap(),
|
position.profit_state().unwrap(),
|
||||||
position.pl(),
|
position.pl(),
|
||||||
position.pair().quote(),
|
position.pair().quote(),
|
||||||
position.pl_perc(),
|
position.pl_perc(),
|
||||||
stop_percentage
|
self.stop_percentages.get(&position.id()).unwrap_or(&0.0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 < ¤t_trailing_delta {
|
|
||||||
self.stop_percentages
|
|
||||||
.insert(position.id(), current_trailing_delta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TrailingStop {
|
impl Default for HiddenTrailingStop {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let leverage = 15.0;
|
let leverage = 5.0;
|
||||||
|
|
||||||
// in percentage
|
// in percentage
|
||||||
let capital_min_profit = 8.5;
|
let capital_max_loss = 15.0;
|
||||||
let capital_max_loss = capital_min_profit * 1.9;
|
let capital_min_profit = 9.0;
|
||||||
let capital_good_profit = capital_min_profit * 2.0;
|
let capital_good_profit = capital_min_profit * 2.0;
|
||||||
|
|
||||||
let weighted_min_profit = capital_min_profit / leverage;
|
let weighted_min_profit = capital_min_profit / leverage;
|
||||||
@ -195,10 +142,8 @@ impl Default for TrailingStop {
|
|||||||
let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
|
let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
|
||||||
let max_loss_percentage = -weighted_max_loss;
|
let max_loss_percentage = -weighted_max_loss;
|
||||||
|
|
||||||
TrailingStop {
|
HiddenTrailingStop {
|
||||||
stop_percentages: Default::default(),
|
stop_percentages: Default::default(),
|
||||||
stop_loss_flags: Default::default(),
|
|
||||||
trail_set_flags: Default::default(),
|
|
||||||
capital_max_loss,
|
capital_max_loss,
|
||||||
capital_min_profit,
|
capital_min_profit,
|
||||||
capital_good_profit,
|
capital_good_profit,
|
||||||
@ -211,10 +156,9 @@ impl Default for TrailingStop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl PositionStrategy for HiddenTrailingStop {
|
||||||
impl PositionStrategy for TrailingStop {
|
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"Trailing Stop".into()
|
"Hidden Trailing Stop".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the profit state of an open position
|
/// Sets the profit state of an open position
|
||||||
@ -223,22 +167,20 @@ impl PositionStrategy for TrailingStop {
|
|||||||
position: Position,
|
position: Position,
|
||||||
current_tick: u64,
|
current_tick: u64,
|
||||||
positions_history: &HashMap<u64, Position>,
|
positions_history: &HashMap<u64, Position>,
|
||||||
_: &[TradingFees],
|
|
||||||
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
||||||
let pl_perc = position.pl_perc();
|
let pl_perc = position.pl_perc();
|
||||||
|
|
||||||
// setting the state of the position based on its profit/loss percentage
|
|
||||||
let state = {
|
let state = {
|
||||||
if pl_perc > self.good_profit_percentage {
|
if pl_perc > self.good_profit_percentage {
|
||||||
Profit
|
PositionProfitState::Profit
|
||||||
} else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
|
} else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
|
||||||
MinimumProfit
|
PositionProfitState::MinimumProfit
|
||||||
} else if (0.0..self.min_profit_percentage).contains(&pl_perc) {
|
} 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) {
|
} else if (self.max_loss_percentage..0.0).contains(&pl_perc) {
|
||||||
Loss
|
PositionProfitState::Loss
|
||||||
} else {
|
} else {
|
||||||
Critical
|
PositionProfitState::Critical
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -246,53 +188,54 @@ impl PositionStrategy for TrailingStop {
|
|||||||
let event_metadata = EventMetadata::new(Some(position.id()), None);
|
let event_metadata = EventMetadata::new(Some(position.id()), None);
|
||||||
let new_position = position.with_profit_state(Some(state));
|
let new_position = position.with_profit_state(Some(state));
|
||||||
|
|
||||||
// checking if there was a state change between the current position
|
|
||||||
// and its last state
|
|
||||||
match opt_prev_position {
|
match opt_prev_position {
|
||||||
Some(prev) => {
|
Some(prev) => {
|
||||||
if prev.profit_state() == Some(state) {
|
if prev.profit_state() == Some(state) {
|
||||||
return (new_position, None, None);
|
return (new_position, None, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrailingStop::play_sound_on_state(&prev, &new_position);
|
|
||||||
}
|
}
|
||||||
None => return (new_position, None, None),
|
None => return (new_position, None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = match state {
|
let events = {
|
||||||
PositionProfitState::Critical => {
|
let mut events = vec![];
|
||||||
Event::new(
|
|
||||||
EventKind::ReachedMaxLoss,
|
if state == PositionProfitState::Profit {
|
||||||
current_tick,
|
events.push(Event::new(
|
||||||
)
|
|
||||||
}
|
|
||||||
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,
|
EventKind::ReachedGoodProfit,
|
||||||
current_tick,
|
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(
|
fn post_tick(
|
||||||
@ -300,89 +243,284 @@ impl PositionStrategy for TrailingStop {
|
|||||||
position: Position,
|
position: Position,
|
||||||
_: u64,
|
_: u64,
|
||||||
_: &HashMap<u64, Position>,
|
_: &HashMap<u64, Position>,
|
||||||
fees: &[TradingFees],
|
|
||||||
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
|
||||||
let taker_fee = fees
|
let close_message = ActionMessage::ClosePosition {
|
||||||
.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 {
|
|
||||||
position_id: position.id(),
|
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
|
// let's check if we surpassed an existing stop percentage
|
||||||
if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
|
if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
|
||||||
if &position.pl_perc() <= existing_stop_percentage {
|
if &position.pl_perc() <= existing_stop_percentage {
|
||||||
info!("Stop percentage surpassed. Closing position.");
|
info!("Stop percentage surpassed. Closing position.");
|
||||||
messages.push(close_position_msg);
|
return (position, None, Some(vec![close_message]));
|
||||||
return (position, None, Some(messages));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(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
|
* ORDER STRATEGIES
|
||||||
*/
|
*/
|
||||||
@ -397,7 +535,7 @@ pub struct MarketEnforce {
|
|||||||
impl Default for MarketEnforce {
|
impl Default for MarketEnforce {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
threshold: 1.2 / 15.0,
|
threshold: 1.0 / 15.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,7 +554,7 @@ impl OrderStrategy for MarketEnforce {
|
|||||||
|
|
||||||
// long
|
// long
|
||||||
let offer_comparison = {
|
let offer_comparison = {
|
||||||
if order.order_form().is_long() {
|
if order.order_form().amount() > 0.0 {
|
||||||
order_book.highest_bid()
|
order_book.highest_bid()
|
||||||
} else {
|
} else {
|
||||||
order_book.lowest_ask()
|
order_book.lowest_ask()
|
||||||
|
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