diff --git a/rustybot/src/connectors.rs b/rustybot/src/connectors.rs index 26aacfa..5b84ee7 100644 --- a/rustybot/src/connectors.rs +++ b/rustybot/src/connectors.rs @@ -17,7 +17,7 @@ use tokio::time::Duration; use crate::currency::{Symbol, SymbolPair}; use crate::models::{ ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position, - PositionState, PriceTicker, Trade, TradingPlatform, WalletKind, + PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind, }; use crate::BoxError; @@ -57,21 +57,56 @@ impl Client { pair: &SymbolPair, ) -> Result>, BoxError> { // retrieving open positions and order book to calculate effective profit/loss - let (positions, order_book) = tokio::join!( + let (positions, order_book, fees) = tokio::join!( self.inner.active_positions(pair), - self.inner.order_book(pair) + self.inner.order_book(pair), + self.inner.trading_fees() ); - let (mut positions, order_book) = (positions?, order_book?); + let (mut positions, order_book, fees) = (positions?, order_book?, fees?); let (best_ask, best_bid) = (order_book.lowest_ask(), order_book.highest_bid()); + if positions.is_none() { + return Ok(None); + } + + let derivative_taker = fees + .iter() + .filter_map(|x| match x { + TradingFees::Taker { + platform, + percentage, + } if platform == &TradingPlatform::Derivative => Some(percentage), + _ => None, + }) + .next() + .ok_or("Could not retrieve derivative taker fee!")?; + let margin_taker = fees + .iter() + .filter_map(|x| match x { + TradingFees::Taker { + platform, + percentage, + } if platform == &TradingPlatform::Margin => Some(percentage), + _ => None, + }) + .next() + .ok_or("Could not retrieve margin taker fee!")?; + // updating positions with effective profit/loss - // TODO: change fee with account's taker fee positions.iter_mut().flatten().for_each(|x| { + let fee = match x.platform() { + TradingPlatform::Funding | TradingPlatform::Exchange => { + unimplemented!() + } + TradingPlatform::Margin => margin_taker, + TradingPlatform::Derivative => derivative_taker, + }; + if x.is_short() { - x.update_profit_loss(best_ask, 0.2); + x.update_profit_loss(best_ask, *fee); } else { - x.update_profit_loss(best_bid, 0.2); + x.update_profit_loss(best_bid, *fee); } }); @@ -88,7 +123,7 @@ impl Client { .active_orders(pair) .await? .into_iter() - .filter(|x| &x.symbol == pair) + .filter(|x| &x.pair() == &pair) .collect()) } @@ -153,6 +188,7 @@ pub trait Connector: Send + Sync { &self, pair: &SymbolPair, ) -> Result>, BoxError>; + async fn trading_fees(&self) -> Result, BoxError>; } impl Debug for dyn Connector { @@ -176,7 +212,7 @@ impl BitfinexConnector { if e.to_string().contains("nonce: small") { return RetryPolicy::WaitRetry(Duration::from_millis(1)); } - return RetryPolicy::ForwardError(e); + RetryPolicy::ForwardError(e) } pub fn new(api_key: &str, api_secret: &str) -> Self { @@ -186,7 +222,9 @@ impl BitfinexConnector { } fn format_trading_pair(pair: &SymbolPair) -> String { - if pair.to_string().to_lowercase().contains("test") { + if pair.to_string().to_lowercase().contains("test") + || pair.to_string().to_lowercase().contains("f0") + { format!("{}:{}", pair.base(), pair.quote()) } else { format!("{}{}", pair.base(), pair.quote()) @@ -275,37 +313,48 @@ impl Connector for BitfinexConnector { async fn submit_order(&self, order: &OrderForm) -> Result { let symbol_name = format!("t{}", BitfinexConnector::format_trading_pair(order.pair())); + let amount = order.amount(); - let order_form = match order.kind() { - OrderKind::Limit { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + let order_form = { + let pre_leverage = { + match order.kind() { + OrderKind::Limit { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::Market => { + bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) + } + OrderKind::Stop { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::StopLimit { price, limit_price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + .with_price_aux_limit(limit_price)? + } + OrderKind::TrailingStop { distance } => { + bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) + .with_price_trailing(distance)? + } + OrderKind::FillOrKill { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + OrderKind::ImmediateOrCancel { price } => { + bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) + } + } + .with_meta(OrderMeta::new( + BitfinexConnector::AFFILIATE_CODE.to_string(), + )) + }; + + // adding leverage, if any + match order.leverage() { + // TODO: CHANGEME!!!! + Some(_leverage) => pre_leverage.with_leverage(15), + // Some(leverage) => pre_leverage.with_leverage(leverage.round() as u32), + None => pre_leverage, } - OrderKind::Market { amount } => { - bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) - } - OrderKind::Stop { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - OrderKind::StopLimit { - price, - amount, - limit_price, - } => bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - .with_price_aux_limit(limit_price)?, - OrderKind::TrailingStop { distance, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, 0.0, amount, order.into()) - .with_price_trailing(distance)? - } - OrderKind::FillOrKill { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - OrderKind::ImmediateOrCancel { price, amount } => { - bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) - } - } - .with_meta(OrderMeta::new( - BitfinexConnector::AFFILIATE_CODE.to_string(), - )); + }; let response = BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?; @@ -371,11 +420,55 @@ impl Connector for BitfinexConnector { Ok((!mapped_vec.is_empty()).then_some(mapped_vec)) } + + async fn trading_fees(&self) -> Result, BoxError> { + let mut fees = vec![]; + let accountfees = + BitfinexConnector::retry_nonce(|| self.bfx.account.account_summary()).await?; + + // Derivatives + let derivative_taker = TradingFees::Taker { + platform: TradingPlatform::Derivative, + percentage: accountfees.derivative_taker() * 100.0, + }; + let derivative_maker = TradingFees::Maker { + platform: TradingPlatform::Derivative, + percentage: accountfees.derivative_rebate() * 100.0, + }; + fees.push(derivative_taker); + fees.push(derivative_maker); + + // Exchange + let exchange_taker = TradingFees::Taker { + platform: TradingPlatform::Exchange, + percentage: accountfees.taker_to_fiat() * 100.0, + }; + let exchange_maker = TradingFees::Maker { + platform: TradingPlatform::Exchange, + percentage: accountfees.maker_fee() * 100.0, + }; + fees.push(exchange_taker); + fees.push(exchange_maker); + + // Margin + let margin_taker = TradingFees::Taker { + platform: TradingPlatform::Margin, + percentage: accountfees.taker_to_fiat() * 100.0, + }; + let margin_maker = TradingFees::Maker { + platform: TradingPlatform::Margin, + percentage: accountfees.maker_fee() * 100.0, + }; + fees.push(margin_taker); + fees.push(margin_maker); + + Ok(fees) + } } impl From<&ActiveOrder> for CancelOrderForm { fn from(o: &ActiveOrder) -> Self { - Self::from_id(o.id) + Self::from_id(o.id()) } } @@ -383,20 +476,18 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder { type Error = BoxError; fn try_from(response: &OrderResponse) -> Result { - Ok(Self { - exchange: Exchange::Bitfinex, - id: response.id(), - group_id: response.gid(), - client_id: Some(response.cid()), - symbol: SymbolPair::from_str(response.symbol())?, - details: OrderForm::new( - SymbolPair::from_str(response.symbol())?, - response.into(), - response.into(), - ), - creation_timestamp: 0, - update_timestamp: 0, - }) + let pair = SymbolPair::from_str(response.symbol())?; + + Ok(ActiveOrder::new( + Exchange::Bitfinex, + response.id(), + pair.clone(), + OrderForm::new(pair, response.into(), response.into(), response.amount()), + response.mts_create(), + response.mts_update(), + ) + .with_group_id(response.gid()) + .with_client_id(Some(response.cid()))) } } @@ -412,6 +503,15 @@ impl TryInto for bitfinex::positions::Position { } }; + let platform = { + if self.symbol().to_ascii_lowercase().contains("f0") { + TradingPlatform::Derivative + } else { + TradingPlatform::Margin + } + }; + + println!("leverage: {}", self.leverage()); Ok(Position::new( SymbolPair::from_str(self.symbol())?, state, @@ -421,6 +521,8 @@ impl TryInto for bitfinex::positions::Position { self.pl_perc(), self.price_liq(), self.position_id(), + platform, + self.leverage(), ) .with_creation_date(self.mts_create()) .with_creation_update(self.mts_update())) @@ -439,7 +541,7 @@ impl From<&OrderForm> for bitfinex::orders::OrderKind { OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::ExchangeFok, OrderKind::ImmediateOrCancel { .. } => bitfinex::orders::OrderKind::ExchangeIoc, }, - TradingPlatform::Margin => match o.kind() { + TradingPlatform::Margin | TradingPlatform::Derivative => match o.kind() { OrderKind::Limit { .. } => bitfinex::orders::OrderKind::Limit, OrderKind::Market { .. } => bitfinex::orders::OrderKind::Market, OrderKind::Stop { .. } => bitfinex::orders::OrderKind::Stop, @@ -489,41 +591,33 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => { Self::Limit { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { - Self::Market { - amount: response.amount(), - } + Self::Market } bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { Self::Stop { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { price: response.price(), - amount: response.amount(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { distance: response.price_trailing().expect("Distance not found!"), - amount: response.amount(), }, bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { Self::FillOrKill { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { Self::ImmediateOrCancel { price: response.price(), - amount: response.amount(), } } } @@ -536,41 +630,33 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => { Self::Limit { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { - Self::Market { - amount: response.amount(), - } + Self::Market {} } bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { Self::Stop { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::StopLimit | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { price: response.price(), - amount: response.amount(), limit_price: response.price_aux_limit().expect("Limit price not found!"), }, bitfinex::orders::OrderKind::TrailingStop | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { distance: response.price_trailing().expect("Distance not found!"), - amount: response.amount(), }, bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { Self::FillOrKill { price: response.price(), - amount: response.amount(), } } bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { Self::ImmediateOrCancel { price: response.price(), - amount: response.amount(), } } } @@ -581,16 +667,16 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder { fn from(order: &bitfinex::orders::ActiveOrder) -> Self { let pair = SymbolPair::from_str(&order.symbol()).expect("Invalid symbol!"); - Self { - exchange: Exchange::Bitfinex, - id: order.id(), - group_id: order.group_id().map(|x| x as u64), - client_id: Some(order.client_id()), - symbol: pair.clone(), - details: OrderForm::new(pair, order.into(), order.into()), - creation_timestamp: order.creation_timestamp(), - update_timestamp: order.update_timestamp(), - } + ActiveOrder::new( + Exchange::Bitfinex, + order.id(), + pair.clone(), + OrderForm::new(pair, order.into(), order.into(), order.amount()), + order.creation_timestamp(), + order.update_timestamp(), + ) + .with_client_id(Some(order.client_id())) + .with_group_id(order.group_id()) } } @@ -637,23 +723,23 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderDetails { // TODO: fields are hardcoded, to fix impl From<&bitfinex::responses::TradeResponse> for Trade { fn from(response: &TradeResponse) -> Self { - let pair = SymbolPair::from_str(&response.symbol).unwrap(); + let pair = SymbolPair::from_str(&response.symbol()).unwrap(); let fee = { - if response.is_maker { - OrderFee::Maker(response.fee) + if response.is_maker() { + OrderFee::Maker(response.fee()) } else { - OrderFee::Taker(response.fee) + OrderFee::Taker(response.fee()) } }; Self { - trade_id: response.trade_id, + trade_id: response.trade_id(), pair, - execution_timestamp: response.execution_timestamp, - price: response.execution_price, - amount: response.execution_amount, + execution_timestamp: response.execution_timestamp(), + price: response.execution_price(), + amount: response.execution_amount(), fee, - fee_currency: Symbol::new(response.symbol.clone()), + fee_currency: Symbol::new(response.symbol().to_owned()), } } } diff --git a/rustybot/src/currency.rs b/rustybot/src/currency.rs index 6f1d4a1..bed1115 100644 --- a/rustybot/src/currency.rs +++ b/rustybot/src/currency.rs @@ -28,10 +28,17 @@ impl Symbol { pub const LTC: Symbol = Symbol::new_static("LTC"); pub const DOT: Symbol = Symbol::new_static("DOT"); + pub const DERIV_BTC: Symbol = Symbol::new_static("BTCF0"); + pub const DERIV_ETH: Symbol = Symbol::new_static("ETHF0"); + pub const DERIV_USDT: Symbol = Symbol::new_static("USTF0"); + // Paper trading pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); pub const TESTUSD: Symbol = Symbol::new_static("TESTUSD"); + pub const DERIV_TESTBTC: Symbol = Symbol::new_static("TESTBTCF0"); + pub const DERIV_TESTUSDT: Symbol = Symbol::new_static("TESTUSDTF0"); + // Fiat coins pub const USD: Symbol = Symbol::new_static("USD"); pub const GBP: Symbol = Symbol::new_static("GBP"); diff --git a/rustybot/src/events.rs b/rustybot/src/events.rs index 1fc24cd..a0f370c 100644 --- a/rustybot/src/events.rs +++ b/rustybot/src/events.rs @@ -5,15 +5,16 @@ use crate::models::OrderForm; #[derive(Debug)] pub struct ActorMessage { - pub(crate) message: Message, + pub(crate) message: ActionMessage, pub(crate) respond_to: oneshot::Sender, } #[derive(Debug)] -pub enum Message { +pub enum ActionMessage { Update { tick: u64 }, ClosePosition { position_id: u64 }, SubmitOrder { order: OrderForm }, + ClosePositionOrders { position_id: u64 }, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] diff --git a/rustybot/src/main.rs b/rustybot/src/main.rs index 8a0d3e5..3f76b66 100644 --- a/rustybot/src/main.rs +++ b/rustybot/src/main.rs @@ -1,6 +1,8 @@ #![feature(drain_filter)] #![feature(bool_to_option)] +use std::env; + use fern::colors::{Color, ColoredLevelConfig}; use log::LevelFilter::{Debug, Trace}; use tokio::time::Duration; @@ -23,33 +25,27 @@ pub type BoxError = Box; #[tokio::main] async fn main() -> Result<(), BoxError> { setup_logger()?; + dotenv::dotenv()?; - // TEST - let test_api_key = "P1EVE68DJByDAkGQvpIkTwfrbYXd2Vo2ZaIhTYb9vx2"; - let test_api_secret = "1nicg8z0zKVEt5Rb7ZDpIYjVYVTgvCaCPMZqB0niFli"; - - // REAL - // let orders_api_key = "hc5nDvYbFYJZMKdnzYq8P4AzCSwjxfQHnMyrg69Sf4c"; - // let orders_api_secret = "53x9goIOpbOtBoPi7dmigK5Cq5e0282EUO2qRIMEXlh"; - // let prices_api_key = "gTfFZUCwRBE0Z9FZjyk9HNe4lZ7XuiZY9rrW71SyUr9"; - // let prices_api_secret = "zWbxvoFZad3BPIiXK4DKfEvC0YsAuaApbeAyI8OBXgN"; - // let positions_api_key = "PfR7BadPZPNdVZnkHFBfAjsg7gjt8pAecMj5B8eRPFi"; - // let positions_api_secret = "izzvxtE3XsBBRpVCHGJ8f60UA56SmPNbBvJGVd67aqD"; + let api_key = env::vars() + .find(|(k, v)| k == "API_KEY") + .map(|(k, v)| v) + .ok_or("API_KEY not set!")?; + let api_secret = env::vars() + .find(|(k, v)| k == "API_SECRET") + .map(|(k, v)| v) + .ok_or("API_SECRET not set!")?; let bitfinex = ExchangeDetails::Bitfinex { - prices_api_key: test_api_key.into(), - prices_api_secret: test_api_secret.into(), - orders_api_key: test_api_key.into(), - orders_api_secret: test_api_secret.into(), - positions_api_key: test_api_key.into(), - positions_api_secret: test_api_secret.into(), + api_key: api_key.into(), + api_secret: api_secret.into(), }; let mut bot = BfxBot::new( vec![bitfinex], - vec![Symbol::BTC, Symbol::ETH, Symbol::XMR], - Symbol::USD, - Duration::new(1, 0), + vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC], + Symbol::DERIV_USDT, + Duration::new(10, 0), ); Ok(bot.start_loop().await?) diff --git a/rustybot/src/managers.rs b/rustybot/src/managers.rs index bf34e82..e2ceee2 100644 --- a/rustybot/src/managers.rs +++ b/rustybot/src/managers.rs @@ -12,14 +12,14 @@ use tokio::time::Duration; use crate::connectors::{Client, ExchangeDetails}; use crate::currency::SymbolPair; -use crate::events::{ActorMessage, Event, Message}; +use crate::events::{ActionMessage, ActorMessage, Event}; use crate::models::{ - ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, TradingPlatform, + ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker, }; -use crate::strategy::{FastOrderStrategy, OrderStrategy, PositionStrategy, TrailingStop}; +use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy}; use crate::BoxError; -pub type OptionUpdate = (Option>, Option>); +pub type OptionUpdate = (Option>, Option>); /****************** * PRICES @@ -33,6 +33,48 @@ pub struct PriceManager { client: Client, } +impl PriceManager { + pub fn new(receiver: Receiver, pair: SymbolPair, client: Client) -> Self { + PriceManager { + receiver, + pair, + prices: Vec::new(), + client, + } + } + + pub async fn handle_message(&mut self, message: ActorMessage) -> Result<(), BoxError> { + if let ActionMessage::Update { tick } = message.message { + let a = self.update(tick).await?; + self.add_entry(a); + } + + Ok(message + .respond_to + .send((None, None)) + .map_err(|_| BoxError::from("Could not send message."))?) + } + + pub fn add_entry(&mut self, entry: PriceEntry) { + self.prices.push(entry); + } + + pub async fn update(&mut self, tick: u64) -> Result { + let current_prices = self.client.current_prices(&self.pair).await?.into(); + + Ok(PriceEntry::new( + tick, + current_prices, + self.pair.clone(), + None, + )) + } + + pub fn pair(&self) -> &SymbolPair { + &self.pair + } +} + pub struct PriceManagerHandle { sender: Sender, } @@ -58,7 +100,7 @@ impl PriceManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -67,51 +109,6 @@ impl PriceManagerHandle { } } -impl PriceManager { - pub fn new(receiver: Receiver, pair: SymbolPair, client: Client) -> Self { - PriceManager { - receiver, - pair, - prices: Vec::new(), - client, - } - } - - pub async fn handle_message(&mut self, message: ActorMessage) -> Result<(), BoxError> { - match message.message { - Message::Update { tick } => { - let a = self.update(tick).await?; - self.add_entry(a); - } - _ => {} - } - - Ok(message - .respond_to - .send((None, None)) - .map_err(|_| BoxError::from("Could not send message."))?) - } - - pub fn add_entry(&mut self, entry: PriceEntry) { - self.prices.push(entry); - } - - pub async fn update(&mut self, tick: u64) -> Result { - let current_prices = self.client.current_prices(&self.pair).await?.into(); - - Ok(PriceEntry::new( - tick, - current_prices, - self.pair.clone(), - None, - )) - } - - pub fn pair(&self) -> &SymbolPair { - &self.pair - } -} - #[derive(Clone, Debug)] pub struct PriceEntry { tick: u64, @@ -179,7 +176,7 @@ impl PositionManagerHandle { self.sender .send(ActorMessage { - message: Message::Update { tick }, + message: ActionMessage::Update { tick }, respond_to: send, }) .await?; @@ -224,7 +221,7 @@ impl PositionManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { tick } => self.update(tick).await?, + ActionMessage::Update { tick } => self.update(tick).await?, _ => (None, None), }; @@ -247,11 +244,7 @@ impl PositionManager { Some(positions) => { // checking if there are positions open for our pair - match positions - .into_iter() - .filter(|x| x.pair() == &self.pair) - .next() - { + match positions.into_iter().find(|x| x.pair() == &self.pair) { // no open positions for our pair, setting active position to none None => { self.active_position = None; @@ -300,10 +293,7 @@ impl PositionManager { None => self.current_tick() - 1, }; - self.positions_history - .get(&tick) - .filter(|x| x.id() == id) - .and_then(|x| Some(x)) + self.positions_history.get(&tick).filter(|x| x.id() == id) } } @@ -312,7 +302,7 @@ impl PositionManager { ******************/ // Position ID: Order ID -pub type TrackedPositionsMap = HashMap; +pub type TrackedPositionsMap = HashMap>; pub struct OrderManagerHandle { sender: Sender, @@ -354,7 +344,36 @@ impl OrderManagerHandle { self.sender .send(ActorMessage { - message: Message::ClosePosition { position_id }, + message: ActionMessage::ClosePosition { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn close_position_orders( + &mut self, + position_id: u64, + ) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::ClosePositionOrders { position_id }, + respond_to: send, + }) + .await?; + + Ok(recv.await?) + } + + pub async fn submit_order(&mut self, order_form: OrderForm) -> Result { + let (send, recv) = oneshot::channel(); + + self.sender + .send(ActorMessage { + message: ActionMessage::SubmitOrder { order: order_form }, respond_to: send, }) .await?; @@ -391,9 +410,14 @@ impl OrderManager { pub async fn handle_message(&mut self, msg: ActorMessage) -> Result<(), BoxError> { let (events, messages) = match msg.message { - Message::Update { .. } => self.update().await?, - Message::ClosePosition { position_id } => self.close_position(position_id).await?, - _ => (None, None), + ActionMessage::Update { .. } => self.update().await?, + ActionMessage::ClosePosition { position_id } => { + self.close_position(position_id).await? + } + ActionMessage::ClosePositionOrders { position_id } => { + self.close_position_orders(position_id).await? + } + ActionMessage::SubmitOrder { order } => self.submit_order(&order).await?, }; Ok(msg @@ -402,6 +426,53 @@ impl OrderManager { .map_err(|_| BoxError::from("Could not send message."))?) } + pub async fn close_position_orders(&self, position_id: u64) -> Result { + info!("Closing outstanding orders for position #{}", position_id); + + if let Some(position_orders) = self.tracked_positions.get(&position_id) { + // retrieving open orders + let open_orders = self.client.active_orders(&self.pair).await?; + let position_orders: Vec<_> = position_orders + .iter() + .filter_map(|&x| open_orders.iter().find(|y| y.id() == x)) + .collect(); + + for order in position_orders { + match self.client.cancel_order(order).await { + Ok(_) => info!("Order #{} closed successfully.", order.id()), + Err(e) => error!("Could not close order #{}: {}", order.id(), e), + } + } + } + + // TODO: return valid messages and events! + Ok((None, None)) + } + + pub async fn submit_order(&mut self, order_form: &OrderForm) -> Result { + info!("Submiting {}", order_form.kind()); + + let active_order = self.client.submit_order(order_form).await?; + + debug!("Adding order to tracked orders."); + if let Some(metadata) = order_form.metadata() { + if let Some(position_id) = metadata.position_id() { + match self.tracked_positions.get_mut(&position_id) { + None => { + self.tracked_positions + .insert(position_id, vec![active_order.id()]); + } + Some(position_orders) => { + position_orders.push(active_order.id()); + } + } + } + }; + + // TODO: return valid messages and events!111!!!1! + Ok((None, None)) + } + pub async fn close_position(&mut self, position_id: u64) -> Result { info!("Closing position #{}", position_id); @@ -421,25 +492,28 @@ impl OrderManager { if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) { let opt_position_order = open_orders .iter() - .find(|x| x.details.amount().neg() == position.amount()); + // avoid using direct equality, using error margin instead + .find(|x| { + (x.order_form().amount().neg() - position.amount()).abs() < 0.0000001 + }); // checking if the position has an open order. // If so, don't do anything since the order is taken care of // in the update phase. // If no order is open, send an undercut limit order at the best current price. - if let None = opt_position_order { + if opt_position_order.is_none() { // No open order, undercutting best price with limit order let closing_price = self.best_closing_price(&position, &order_book); - // TODO: hardcoded platform to Margin! let order_form = OrderForm::new( self.pair.clone(), OrderKind::Limit { price: closing_price, - amount: position.amount().neg(), }, - TradingPlatform::Margin, - ); + position.platform(), + position.amount().neg(), + ) + .with_leverage(Some(position.leverage())); info!("Submitting {} order", order_form.kind()); if let Err(e) = self.client.submit_order(&order_form).await { @@ -458,8 +532,8 @@ impl OrderManager { Ok((None, None)) } - pub async fn update(&self) -> Result { - trace!("\t[OrderManager] Updating {}", self.pair); + pub async fn update(&mut self) -> Result { + debug!("\t[OrderManager] Updating {}", self.pair); let (res_open_orders, res_order_book) = tokio::join!( self.client.active_orders(&self.pair), @@ -468,8 +542,49 @@ impl OrderManager { let (open_orders, order_book) = (res_open_orders?, res_order_book?); + // retrieving open positions to check whether the positions have open orders. + // we need to update our internal mapping in that case. + if !open_orders.is_empty() { + let open_positions = self.client.active_positions(&self.pair).await?; + + if let Some(positions) = open_positions { + // currently, we are only trying to match orders with an amount equal to + // a position amount. + for position in positions { + let matching_order = open_orders + .iter() + .find(|x| x.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 { - debug!( + trace!( "Found open order, calling \"{}\" strategy.", self.strategy.name() ); @@ -479,9 +594,9 @@ impl OrderManager { if let Some(messages) = strat_messages { for m in messages { match m { - Message::SubmitOrder { order: order_form } => { + ActionMessage::SubmitOrder { order: order_form } => { info!("Closing open order..."); - info!("\tCancelling open order #{}", &active_order.id); + info!("\tCancelling open order #{}", &active_order.id()); self.client.cancel_order(&active_order).await?; info!("\tSubmitting {}...", order_form.kind()); @@ -508,26 +623,22 @@ impl OrderManager { let delta = (ask - bid) / 10.0; let closing_price = { - let closing_price = { - if position.is_short() { - bid - delta - } else { - ask + delta - } - }; - - if avg > 9999.0 { - if position.is_short() { - closing_price.ceil() - } else { - closing_price.floor() - } + if position.is_short() { + bid - delta } else { - closing_price + ask + delta } }; - closing_price + if avg > 9999.0 { + if position.is_short() { + closing_price.ceil() + } else { + closing_price.floor() + } + } else { + closing_price + } } } @@ -546,12 +657,12 @@ impl PairManager { order_manager: OrderManagerHandle::new( pair.clone(), client.clone(), - Box::new(FastOrderStrategy::default()), + Box::new(MarketEnforce::default()), ), position_manager: PositionManagerHandle::new( - pair.clone(), - client.clone(), - Box::new(TrailingStop::new()), + pair, + client, + Box::new(HiddenTrailingStop::default()), ), } } @@ -578,10 +689,18 @@ impl PairManager { if let Some(messages) = messages { for m in messages { match m { - Message::ClosePosition { position_id } => { + ActionMessage::Update { .. } => {} + ActionMessage::ClosePosition { position_id } => { self.order_manager.close_position(position_id).await?; } - _ => {} + ActionMessage::SubmitOrder { order } => { + self.order_manager.submit_order(order).await?; + } + ActionMessage::ClosePositionOrders { position_id } => { + self.order_manager + .close_position_orders(position_id) + .await?; + } } } } @@ -593,21 +712,19 @@ impl PairManager { pub struct ExchangeManager { kind: ExchangeDetails, pair_managers: Vec, - client: Client, } impl ExchangeManager { - pub fn new(kind: &ExchangeDetails, pairs: &Vec) -> Self { + pub fn new(kind: &ExchangeDetails, pairs: &[SymbolPair]) -> Self { let client = Client::new(kind); let pair_managers = pairs - .into_iter() + .iter() .map(|x| PairManager::new(x.clone(), client.clone())) .collect(); Self { kind: kind.clone(), pair_managers, - client, } } @@ -619,7 +736,7 @@ impl ExchangeManager { .collect(); // execute the futures - while let Some(_) = futures.next().await {} + while futures.next().await.is_some() {} Ok(()) } diff --git a/rustybot/src/models.rs b/rustybot/src/models.rs index 1e137b5..2eae4bd 100644 --- a/rustybot/src/models.rs +++ b/rustybot/src/models.rs @@ -139,18 +139,6 @@ impl OrderDetails { } } - pub fn exchange(&self) -> Exchange { - self.exchange - } - pub fn platform(&self) -> TradingPlatform { - self.platform - } - pub fn kind(&self) -> OrderKind { - self.kind - } - pub fn execution_timestamp(&self) -> u64 { - self.execution_timestamp - } pub fn id(&self) -> u64 { self.id } @@ -162,14 +150,71 @@ impl OrderDetails { #[derive(Clone, Debug)] pub struct ActiveOrder { - pub(crate) exchange: Exchange, - pub(crate) id: u64, - pub(crate) group_id: Option, - pub(crate) client_id: Option, - pub(crate) symbol: SymbolPair, - pub(crate) details: OrderForm, - pub(crate) creation_timestamp: u64, - pub(crate) update_timestamp: u64, + exchange: Exchange, + id: u64, + group_id: Option, + client_id: Option, + pair: SymbolPair, + order_form: OrderForm, + creation_timestamp: u64, + update_timestamp: u64, +} + +impl ActiveOrder { + pub fn new( + exchange: Exchange, + id: u64, + pair: SymbolPair, + order_form: OrderForm, + creation_timestamp: u64, + update_timestamp: u64, + ) -> Self { + Self { + exchange, + id, + group_id: None, + client_id: None, + pair, + order_form, + creation_timestamp, + update_timestamp, + } + } + + pub fn with_group_id(mut self, group_id: Option) -> Self { + self.group_id = group_id; + self + } + + pub fn with_client_id(mut self, client_id: Option) -> Self { + self.client_id = client_id; + self + } + + pub fn exchange(&self) -> Exchange { + self.exchange + } + pub fn id(&self) -> u64 { + self.id + } + pub fn group_id(&self) -> Option { + self.group_id + } + pub fn client_id(&self) -> Option { + self.client_id + } + pub fn pair(&self) -> &SymbolPair { + &self.pair + } + pub fn order_form(&self) -> &OrderForm { + &self.order_form + } + pub fn creation_timestamp(&self) -> u64 { + self.creation_timestamp + } + pub fn update_timestamp(&self) -> u64 { + self.update_timestamp + } } impl Hash for ActiveOrder { @@ -186,7 +231,7 @@ impl PartialEq for ActiveOrder { impl Eq for ActiveOrder {} -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum TradingPlatform { Exchange, Derivative, @@ -213,34 +258,13 @@ impl Display for TradingPlatform { #[derive(Copy, Clone, Debug)] pub enum OrderKind { - Limit { - price: f64, - amount: f64, - }, - Market { - amount: f64, - }, - Stop { - price: f64, - amount: f64, - }, - StopLimit { - price: f64, - amount: f64, - limit_price: f64, - }, - TrailingStop { - distance: f64, - amount: f64, - }, - FillOrKill { - price: f64, - amount: f64, - }, - ImmediateOrCancel { - price: f64, - amount: f64, - }, + Limit { price: f64 }, + Market, + Stop { price: f64 }, + StopLimit { price: f64, limit_price: f64 }, + TrailingStop { distance: f64 }, + FillOrKill { price: f64 }, + ImmediateOrCancel { price: f64 }, } impl OrderKind { @@ -260,67 +284,32 @@ impl OrderKind { impl Display for OrderKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - OrderKind::Limit { price, amount } => { + OrderKind::Limit { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) + } + OrderKind::Market => { + write!(f, "[{}]", self.as_str()) + } + OrderKind::Stop { price } => { + write!(f, "[{} | Price: {:0.5}", self.as_str(), price,) + } + OrderKind::StopLimit { price, limit_price } => { write!( f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", + "[{} | Price: {:0.5}, Limit Price: {:0.5}]", self.as_str(), price, - amount - ) - } - OrderKind::Market { amount } => { - write!(f, "[{} | Amount: {:0.5}]", self.as_str(), amount) - } - OrderKind::Stop { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) - } - OrderKind::StopLimit { - price, - amount, - limit_price, - } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}, Limit Price: {:0.5}]", - self.as_str(), - price, - amount, limit_price ) } - OrderKind::TrailingStop { distance, amount } => { - write!( - f, - "[{} | Distance: {:0.5}, Amount: {:0.5}]", - self.as_str(), - distance, - amount - ) + OrderKind::TrailingStop { distance } => { + write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,) } - OrderKind::FillOrKill { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) + OrderKind::FillOrKill { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) } - OrderKind::ImmediateOrCancel { price, amount } => { - write!( - f, - "[{} | Price: {:0.5}, Amount: {:0.5}]", - self.as_str(), - price, - amount - ) + OrderKind::ImmediateOrCancel { price } => { + write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,) } } } @@ -331,17 +320,38 @@ pub struct OrderForm { pair: SymbolPair, kind: OrderKind, platform: TradingPlatform, + amount: f64, + leverage: Option, + metadata: Option, } impl OrderForm { - pub fn new(pair: SymbolPair, order_kind: OrderKind, platform: TradingPlatform) -> Self { + pub fn new( + pair: SymbolPair, + order_kind: OrderKind, + platform: TradingPlatform, + amount: f64, + ) -> Self { Self { pair, kind: order_kind, platform, + amount, + leverage: None, + metadata: None, } } + pub fn with_leverage(mut self, leverage: Option) -> Self { + self.leverage = leverage; + self + } + + pub fn with_metadata(mut self, metadata: Option) -> Self { + self.metadata = metadata; + self + } + pub fn pair(&self) -> &SymbolPair { &self.pair } @@ -355,15 +365,7 @@ impl OrderForm { } pub fn amount(&self) -> f64 { - match self.kind { - OrderKind::Limit { amount, .. } => amount, - OrderKind::Market { amount } => amount, - OrderKind::Stop { amount, .. } => amount, - OrderKind::StopLimit { amount, .. } => amount, - OrderKind::TrailingStop { amount, .. } => amount, - OrderKind::FillOrKill { amount, .. } => amount, - OrderKind::ImmediateOrCancel { amount, .. } => amount, - } + self.amount } pub fn price(&self) -> Option { @@ -377,6 +379,31 @@ impl OrderForm { OrderKind::ImmediateOrCancel { price, .. } => Some(price), } } + + pub fn leverage(&self) -> Option { + self.leverage + } + + pub fn metadata(&self) -> &Option { + &self.metadata + } +} + +#[derive(Debug, Clone)] +pub struct OrderMetadata { + position_id: Option, +} + +impl OrderMetadata { + pub fn with_position_id(position_id: u64) -> Self { + OrderMetadata { + position_id: Some(position_id), + } + } + + pub fn position_id(&self) -> Option { + self.position_id + } } /*************** @@ -396,6 +423,8 @@ pub struct Position { position_id: u64, creation_date: Option, creation_update: Option, + platform: TradingPlatform, + leverage: f64, } impl Position { @@ -408,6 +437,8 @@ impl Position { pl_perc: f64, price_liq: f64, position_id: u64, + platform: TradingPlatform, + leverage: f64, ) -> Self { Position { pair, @@ -421,6 +452,8 @@ impl Position { creation_date: None, creation_update: None, profit_state: None, + platform, + leverage, } } @@ -505,6 +538,12 @@ impl Position { pub fn is_long(&self) -> bool { self.amount.is_sign_positive() } + pub fn platform(&self) -> TradingPlatform { + self.platform + } + pub fn leverage(&self) -> f64 { + self.leverage + } } impl Hash for Position { @@ -530,17 +569,6 @@ pub enum PositionProfitState { Profit, } -impl PositionProfitState { - fn color(self) -> String { - match self { - PositionProfitState::Critical | PositionProfitState::Loss => "red", - PositionProfitState::BreakEven => "yellow", - PositionProfitState::MinimumProfit | PositionProfitState::Profit => "green", - } - .into() - } -} - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum PositionState { Closed, @@ -563,3 +591,15 @@ pub struct Trade { pub fee: OrderFee, pub fee_currency: Symbol, } + +#[derive(Debug)] +pub enum TradingFees { + Maker { + platform: TradingPlatform, + percentage: f64, + }, + Taker { + platform: TradingPlatform, + percentage: f64, + }, +} diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index df9a34a..1052c57 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -4,12 +4,10 @@ use std::fmt::{Debug, Formatter}; use dyn_clone::DynClone; use log::info; -use crate::events::{Event, EventKind, EventMetadata, Message}; +use crate::connectors::Connector; +use crate::events::{ActionMessage, Event, EventKind, EventMetadata}; use crate::managers::OptionUpdate; -use crate::models::{ - ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState, - PositionState, TradingPlatform, -}; +use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState}; use crate::BoxError; /*************** @@ -23,13 +21,13 @@ pub trait PositionStrategy: DynClone + Send + Sync { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); fn post_tick( &mut self, position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>); + ) -> (Position, Option>, Option>); } impl Debug for dyn PositionStrategy { @@ -69,54 +67,54 @@ impl Debug for dyn OrderStrategy { ***************/ #[derive(Clone, Debug)] -pub struct TrailingStop { +pub struct HiddenTrailingStop { stop_percentages: HashMap, + capital_max_loss: f64, + capital_min_profit: f64, + capital_good_profit: f64, + min_profit_trailing_delta: f64, + good_profit_trailing_delta: f64, + leverage: f64, + min_profit_percentage: f64, + good_profit_percentage: f64, + max_loss_percentage: f64, } -impl TrailingStop { - const BREAK_EVEN_PERC: f64 = 0.1; - const MIN_PROFIT_PERC: f64 = 0.5; - const GOOD_PROFIT_PERC: f64 = TrailingStop::MIN_PROFIT_PERC * 1.75; - const MAX_LOSS_PERC: f64 = -4.0; - - pub fn new() -> Self { - TrailingStop { - stop_percentages: HashMap::new(), - } - } - +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(0.2), - PositionProfitState::Profit => Some(0.1), + PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta), + PositionProfitState::Profit => Some(self.good_profit_trailing_delta), _ => None, }; if let Some(profit_state_delta) = profit_state_delta { let current_stop_percentage = position.pl_perc() - profit_state_delta; - match profit_state { - PositionProfitState::MinimumProfit | PositionProfitState::Profit => { - match self.stop_percentages.get(&position.id()) { - None => { + if let PositionProfitState::MinimumProfit | PositionProfitState::Profit = + profit_state + { + match self.stop_percentages.get(&position.id()) { + None => { + self.stop_percentages + .insert(position.id(), current_stop_percentage); + } + Some(existing_threshold) => { + if existing_threshold < ¤t_stop_percentage { self.stop_percentages .insert(position.id(), current_stop_percentage); } - Some(existing_threshold) => { - if existing_threshold < ¤t_stop_percentage { - self.stop_percentages - .insert(position.id(), current_stop_percentage); - } - } } } - _ => {} } } + info!( - "\tState: {:?} | PL%: {:0.2} | Stop: {:0.2}", + "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", position.profit_state().unwrap(), + position.pl(), + position.pair().quote(), position.pl_perc(), self.stop_percentages.get(&position.id()).unwrap_or(&0.0) ); @@ -124,9 +122,43 @@ impl TrailingStop { } } -impl PositionStrategy for TrailingStop { +impl Default for HiddenTrailingStop { + fn default() -> Self { + let leverage = 5.0; + + // in percentage + let capital_max_loss = 15.0; + let capital_min_profit = 9.0; + let capital_good_profit = capital_min_profit * 2.0; + + let weighted_min_profit = capital_min_profit / leverage; + let weighted_good_profit = capital_good_profit / leverage; + let weighted_max_loss = capital_max_loss / leverage; + + let min_profit_trailing_delta = weighted_min_profit * 0.17; + let good_profit_trailing_delta = weighted_good_profit * 0.08; + + let min_profit_percentage = weighted_min_profit + min_profit_trailing_delta; + let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta; + let max_loss_percentage = -weighted_max_loss; + + HiddenTrailingStop { + stop_percentages: Default::default(), + capital_max_loss, + capital_min_profit, + capital_good_profit, + min_profit_trailing_delta, + good_profit_trailing_delta, + leverage, + min_profit_percentage, + good_profit_percentage, + max_loss_percentage, + } + } +} +impl PositionStrategy for HiddenTrailingStop { fn name(&self) -> String { - "Trailing stop".into() + "Hidden Trailing Stop".into() } /// Sets the profit state of an open position @@ -135,19 +167,17 @@ impl PositionStrategy for TrailingStop { position: Position, current_tick: u64, positions_history: &HashMap, - ) -> (Position, Option>, Option>) { + ) -> (Position, Option>, Option>) { let pl_perc = position.pl_perc(); let state = { - if pl_perc > TrailingStop::GOOD_PROFIT_PERC { + if pl_perc > self.good_profit_percentage { PositionProfitState::Profit - } else if TrailingStop::MIN_PROFIT_PERC <= pl_perc - && pl_perc < TrailingStop::GOOD_PROFIT_PERC - { + } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { PositionProfitState::MinimumProfit - } else if 0.0 <= pl_perc && pl_perc < TrailingStop::MIN_PROFIT_PERC { + } else if (0.0..self.min_profit_percentage).contains(&pl_perc) { PositionProfitState::BreakEven - } else if TrailingStop::MAX_LOSS_PERC < pl_perc && pl_perc < 0.0 { + } else if (self.max_loss_percentage..0.0).contains(&pl_perc) { PositionProfitState::Loss } else { PositionProfitState::Critical @@ -156,7 +186,7 @@ impl PositionStrategy for TrailingStop { let opt_prev_position = positions_history.get(&(current_tick - 1)); let event_metadata = EventMetadata::new(Some(position.id()), None); - let new_position = position.clone().with_profit_state(Some(state)); + let new_position = position.with_profit_state(Some(state)); match opt_prev_position { Some(prev) => { @@ -205,7 +235,7 @@ impl PositionStrategy for TrailingStop { events }; - return (new_position, Some(events), None); + (new_position, Some(events), None) } fn post_tick( @@ -213,20 +243,15 @@ impl PositionStrategy for TrailingStop { position: Position, _: u64, _: &HashMap, - ) -> (Position, Option>, Option>) { - let close_message = Message::ClosePosition { + ) -> (Position, Option>, Option>) { + let close_message = ActionMessage::ClosePosition { position_id: position.id(), }; // if critical, early return with close position - if let Some(profit_state) = position.profit_state() { - match profit_state { - PositionProfitState::Critical => { - info!("Maximum loss reached. Closing position."); - return (position, None, Some(vec![close_message])); - } - _ => {} - } + if let Some(PositionProfitState::Critical) = position.profit_state() { + info!("Maximum loss reached. Closing position."); + return (position, None, Some(vec![close_message])); }; // let's check if we surpassed an existing stop percentage @@ -243,28 +268,281 @@ impl PositionStrategy for TrailingStop { } } +// #[derive(Clone, Debug)] +// pub struct TrailingStop { +// stop_percentages: HashMap, +// capital_max_loss: f64, +// capital_min_profit: f64, +// capital_good_profit: f64, +// min_profit_trailing_delta: f64, +// good_profit_trailing_delta: f64, +// leverage: f64, +// min_profit_percentage: f64, +// good_profit_percentage: f64, +// max_loss_percentage: f64, +// } +// +// impl TrailingStop { +// fn update_stop_percentage(&mut self, position: &Position) -> Option { +// let mut order_form = None; +// +// if let Some(profit_state) = position.profit_state() { +// let profit_state_delta = match profit_state { +// PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta), +// PositionProfitState::Profit => Some(self.good_profit_trailing_delta), +// _ => None, +// }; +// +// if let Some(profit_state_delta) = profit_state_delta { +// let current_stop_percentage = position.pl_perc() - profit_state_delta; +// let price_percentage_delta = { +// if position.is_short() { +// // 1.0 is the base price +// 1.0 - current_stop_percentage / 100.0 +// } else { +// 1.0 + current_stop_percentage / 100.0 +// } +// }; +// +// println!("Delta: {}", price_percentage_delta); +// +// if let PositionProfitState::MinimumProfit | PositionProfitState::Profit = +// profit_state +// { +// match self.stop_percentages.get(&position.id()) { +// None => { +// self.stop_percentages +// .insert(position.id(), current_stop_percentage); +// +// trace!("Setting trailing stop, asking order manager to cancel previous orders."); +// order_form = Some( +// OrderForm::new( +// position.pair().clone(), +// OrderKind::Limit { +// price: position.base_price() * price_percentage_delta, +// }, +// position.platform(), +// position.amount().neg(), +// ) +// .with_metadata(OrderMetadata::with_position_id(position.id())), +// ); +// } +// Some(existing_threshold) => { +// // follow and update trailing stop +// if existing_threshold < ¤t_stop_percentage { +// self.stop_percentages +// .insert(position.id(), current_stop_percentage); +// +// trace!("Updating threshold, asking order manager to cancel previous orders."); +// order_form = Some( +// OrderForm::new( +// position.pair().clone(), +// OrderKind::Limit { +// price: position.base_price() * price_percentage_delta, +// }, +// position.platform(), +// position.amount().neg(), +// ) +// .with_metadata(OrderMetadata::with_position_id(position.id())), +// ); +// } +// } +// } +// } +// } +// +// info!( +// "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}", +// position.profit_state().unwrap(), +// position.pl(), +// position.pair().quote(), +// position.pl_perc(), +// self.stop_percentages.get(&position.id()).unwrap_or(&0.0) +// ); +// } +// +// order_form +// } +// } +// +// impl Default for TrailingStop { +// fn default() -> Self { +// let leverage = 5.0; +// +// // in percentage +// let capital_max_loss = 15.0; +// let capital_min_profit = 1.0; +// let capital_good_profit = 6.0; +// +// let weighted_min_profit = capital_min_profit / leverage; +// let weighted_good_profit = capital_good_profit / leverage; +// let weighted_max_loss = capital_max_loss / leverage; +// +// let min_profit_trailing_delta = weighted_min_profit * 0.2; +// let good_profit_trailing_delta = weighted_good_profit * 0.08; +// +// let min_profit_percentage = weighted_min_profit + min_profit_trailing_delta; +// let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta; +// let max_loss_percentage = -weighted_max_loss; +// +// TrailingStop { +// stop_percentages: Default::default(), +// capital_max_loss, +// capital_min_profit, +// capital_good_profit, +// min_profit_trailing_delta, +// good_profit_trailing_delta, +// leverage, +// min_profit_percentage, +// good_profit_percentage, +// max_loss_percentage, +// } +// } +// } +// impl PositionStrategy for TrailingStop { +// fn name(&self) -> String { +// "Trailing Stop".into() +// } +// +// /// Sets the profit state of an open position +// fn on_tick( +// &mut self, +// position: Position, +// current_tick: u64, +// positions_history: &HashMap, +// ) -> (Position, Option>, Option>) { +// let pl_perc = position.pl_perc(); +// +// let state = { +// if pl_perc > self.good_profit_percentage { +// PositionProfitState::Profit +// } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) { +// PositionProfitState::MinimumProfit +// } else if (0.0..self.min_profit_percentage).contains(&pl_perc) { +// PositionProfitState::BreakEven +// } else if (self.max_loss_percentage..0.0).contains(&pl_perc) { +// PositionProfitState::Loss +// } else { +// PositionProfitState::Critical +// } +// }; +// +// let opt_prev_position = positions_history.get(&(current_tick - 1)); +// let event_metadata = EventMetadata::new(Some(position.id()), None); +// let new_position = position.with_profit_state(Some(state)); +// +// match opt_prev_position { +// Some(prev) => { +// if prev.profit_state() == Some(state) { +// return (new_position, None, None); +// } +// } +// None => return (new_position, None, None), +// }; +// +// let events = { +// let mut events = vec![]; +// +// if state == PositionProfitState::Profit { +// events.push(Event::new( +// EventKind::ReachedGoodProfit, +// current_tick, +// Some(event_metadata), +// )); +// } else if state == PositionProfitState::MinimumProfit { +// events.push(Event::new( +// EventKind::ReachedMinProfit, +// current_tick, +// Some(event_metadata), +// )); +// } else if state == PositionProfitState::BreakEven { +// events.push(Event::new( +// EventKind::ReachedBreakEven, +// current_tick, +// Some(event_metadata), +// )); +// } else if state == PositionProfitState::Loss { +// events.push(Event::new( +// EventKind::ReachedLoss, +// current_tick, +// Some(event_metadata), +// )); +// } else { +// events.push(Event::new( +// EventKind::ReachedMaxLoss, +// current_tick, +// Some(event_metadata), +// )); +// } +// +// events +// }; +// +// (new_position, Some(events), None) +// } +// +// fn post_tick( +// &mut self, +// position: Position, +// _: u64, +// _: &HashMap, +// ) -> (Position, Option>, Option>) { +// let close_message = ActionMessage::ClosePosition { +// position_id: position.id(), +// }; +// +// // if critical, early return with close position +// if let Some(PositionProfitState::Critical) = position.profit_state() { +// info!("Maximum loss reached. Closing position."); +// return (position, None, Some(vec![close_message])); +// }; +// +// // let's check if we surpassed an existing stop percentage +// if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) { +// if &position.pl_perc() <= existing_stop_percentage { +// info!("Stop percentage surpassed. Closing position."); +// return (position, None, Some(vec![close_message])); +// } +// } +// +// // updated or new trailing stop. should cancel orders and submit new one +// if let Some(order_form) = self.update_stop_percentage(&position) { +// let mut messages = vec![]; +// +// messages.push(ActionMessage::ClosePositionOrders { +// position_id: position.id(), +// }); +// messages.push(ActionMessage::SubmitOrder { order: order_form }); +// +// return (position, None, Some(messages)); +// } +// +// (position, None, None) +// } +// } + +/* + * ORDER STRATEGIES + */ + #[derive(Clone, Debug)] -pub struct FastOrderStrategy { +pub struct MarketEnforce { // threshold (%) for which we trigger a market order // to close an open position threshold: f64, } -impl Default for FastOrderStrategy { +impl Default for MarketEnforce { fn default() -> Self { - Self { threshold: 0.2 } + Self { + threshold: 1.0 / 15.0, + } } } -impl FastOrderStrategy { - pub fn new(threshold: f64) -> Self { - Self { threshold } - } -} - -impl OrderStrategy for FastOrderStrategy { +impl OrderStrategy for MarketEnforce { fn name(&self) -> String { - "Fast order strategy".into() + "Market Enforce".into() } fn on_open_order( @@ -276,7 +554,7 @@ impl OrderStrategy for FastOrderStrategy { // long let offer_comparison = { - if order.details.amount() > 0.0 { + if order.order_form().amount() > 0.0 { order_book.highest_bid() } else { order_book.lowest_ask() @@ -286,63 +564,24 @@ impl OrderStrategy for FastOrderStrategy { // if the best offer is higher than our threshold, // ask the manager to close the position with a market order let order_price = order - .details + .order_form() .price() .ok_or("The active order does not have a price!")?; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; if delta > self.threshold { - messages.push(Message::SubmitOrder { + messages.push(ActionMessage::SubmitOrder { order: OrderForm::new( - order.symbol.clone(), - OrderKind::Market { - amount: order.details.amount(), - }, - order.details.platform().clone(), - ), + order.pair().clone(), + OrderKind::Market, + *order.order_form().platform(), + order.order_form().amount(), + ) + .with_leverage(order.order_form().leverage()) + .with_metadata(order.order_form().metadata().clone()), }) } Ok((None, (!messages.is_empty()).then_some(messages))) } - - // fn on_position_order( - // &self, - // order: &ActiveOrder, - // _: &Position, - // order_book: &OrderBook, - // ) -> Result { - // let mut messages = vec![]; - // - // // long - // let offer_comparison = { - // if order.current_form.amount() > 0.0 { - // order_book.highest_bid() - // } else { - // order_book.lowest_ask() - // } - // }; - // - // // if the best offer is higher than our threshold, - // // ask the manager to close the position with a market order - // let order_price = order - // .current_form - // .price() - // .ok_or("The active order does not have a price!")?; - // let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; - // - // if delta > self.threshold { - // messages.push(Message::SubmitOrder { - // order: OrderForm::new( - // order.symbol.clone(), - // OrderKind::Market { - // amount: order.current_form.amount(), - // }, - // order.current_form.platform().clone(), - // ), - // }) - // } - // - // Ok((None, (!messages.is_empty()).then_some(messages))) - // } }