implemented stop order for trailing stop. position strategy now receives fees information. added fees API to client.

This commit is contained in:
Giulio De Pasquale 2021-02-20 21:01:32 +00:00
parent 386137f16e
commit 2d81358fa0
3 changed files with 99 additions and 36 deletions

View File

@ -14,12 +14,12 @@ use log::trace;
use tokio::macros::support::Future;
use tokio::time::Duration;
use crate::BoxError;
use crate::currency::{Symbol, SymbolPair};
use crate::models::{
ActiveOrder, OrderBook, OrderBookEntry, OrderDetails, OrderFee, OrderForm, OrderKind, Position,
PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
};
use crate::BoxError;
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Exchange {
@ -164,6 +164,8 @@ impl Client {
) -> Result<Option<Vec<OrderDetails>>, BoxError> {
self.inner.orders_history(pair).await
}
pub async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError> { self.inner.trading_fees().await }
}
#[async_trait]
@ -183,7 +185,7 @@ pub trait Connector: Send + Sync {
amount: f64,
) -> Result<(), BoxError>;
async fn trades_from_order(&self, order: &OrderDetails)
-> Result<Option<Vec<Trade>>, BoxError>;
-> Result<Option<Vec<Trade>>, BoxError>;
async fn orders_history(
&self,
pair: &SymbolPair,
@ -235,9 +237,9 @@ impl BitfinexConnector {
// the function may fail due to concurrent signed requests
// parsed in different times by the server
async fn retry_nonce<F, Fut, O>(mut func: F) -> Result<O, BoxError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<O, BoxError>>,
where
F: FnMut() -> Fut,
Fut: Future<Output=Result<O, BoxError>>,
{
let response = {
loop {
@ -291,7 +293,7 @@ impl Connector for BitfinexConnector {
let response = BitfinexConnector::retry_nonce(|| {
self.bfx.book.trading_pair(&symbol_name, BookPrecision::P0)
})
.await?;
.await?;
let entries = response
.into_iter()
@ -327,7 +329,7 @@ impl Connector for BitfinexConnector {
OrderKind::Stop { price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
}
OrderKind::StopLimit { price, limit_price } => {
OrderKind::StopLimit { stop_price: price, limit_price } => {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
.with_price_aux_limit(limit_price)?
}
@ -342,9 +344,9 @@ impl Connector for BitfinexConnector {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into())
}
}
.with_meta(OrderMeta::new(
BitfinexConnector::AFFILIATE_CODE.to_string(),
))
.with_meta(OrderMeta::new(
BitfinexConnector::AFFILIATE_CODE.to_string(),
))
};
// adding leverage, if any
@ -386,7 +388,7 @@ impl Connector for BitfinexConnector {
amount,
)
})
.await?;
.await?;
Ok(())
}
@ -400,7 +402,7 @@ impl Connector for BitfinexConnector {
.trades
.generated_by_order(order.pair().trading_repr(), order.id())
})
.await?;
.await?;
if response.is_empty() {
Ok(None)
@ -486,8 +488,8 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder {
response.mts_create(),
response.mts_update(),
)
.with_group_id(response.gid())
.with_client_id(Some(response.cid())))
.with_group_id(response.gid())
.with_client_id(Some(response.cid())))
}
}
@ -524,8 +526,8 @@ impl TryInto<Position> for bitfinex::positions::Position {
platform,
self.leverage(),
)
.with_creation_date(self.mts_create())
.with_creation_update(self.mts_update()))
.with_creation_date(self.mts_create())
.with_creation_update(self.mts_update()))
}
}
@ -603,7 +605,7 @@ impl From<&bitfinex::responses::OrderResponse> for OrderKind {
}
bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(),
stop_price: response.price(),
limit_price: response.price_aux_limit().expect("Limit price not found!"),
},
bitfinex::orders::OrderKind::TrailingStop
@ -642,7 +644,7 @@ impl From<&bitfinex::orders::ActiveOrder> for OrderKind {
}
bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(),
stop_price: response.price(),
limit_price: response.price_aux_limit().expect("Limit price not found!"),
},
bitfinex::orders::OrderKind::TrailingStop
@ -675,8 +677,8 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder {
order.creation_timestamp(),
order.update_timestamp(),
)
.with_client_id(Some(order.client_id()))
.with_group_id(order.group_id())
.with_client_id(Some(order.client_id()))
.with_group_id(order.group_id())
}
}

View File

@ -5,11 +5,12 @@ use futures_util::stream::FuturesUnordered;
use futures_util::StreamExt;
use log::{debug, error, info, trace};
use merge::Merge;
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::mpsc::channel;
use tokio::sync::oneshot;
use tokio::time::Duration;
use crate::BoxError;
use crate::connectors::{Client, ExchangeDetails};
use crate::currency::SymbolPair;
use crate::events::{ActionMessage, ActorMessage, Event};
@ -17,7 +18,6 @@ use crate::models::{
ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PriceTicker,
};
use crate::strategy::{HiddenTrailingStop, MarketEnforce, OrderStrategy, PositionStrategy};
use crate::BoxError;
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
@ -234,8 +234,10 @@ impl PositionManager {
pub async fn update(&mut self, tick: u64) -> Result<OptionUpdate, BoxError> {
trace!("\t[PositionManager] Updating {}", self.pair);
let opt_active_positions = self.client.active_positions(&self.pair).await?;
self.current_tick = tick;
let (fees, opt_active_positions) = tokio::join!(self.client.trading_fees(),self.client.active_positions(&self.pair));
let (fees, opt_active_positions) = (fees?, opt_active_positions?);
// we assume there is only ONE active position per pair
match opt_active_positions {
@ -258,11 +260,11 @@ impl PositionManager {
let (pos_on_tick, events_on_tick, messages_on_tick) = self
.strategy
.on_tick(position, self.current_tick(), &self.positions_history);
.on_tick(position, self.current_tick(), &self.positions_history, &fees);
let (pos_post_tick, events_post_tick, messages_post_tick) = self
.strategy
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history);
.post_tick(pos_on_tick, self.current_tick(), &self.positions_history, &fees);
events.merge(events_on_tick);
events.merge(events_post_tick);
@ -513,7 +515,7 @@ impl OrderManager {
position.platform(),
position.amount().neg(),
)
.with_leverage(Some(position.leverage()));
.with_leverage(Some(position.leverage()));
info!("Submitting {} order", order_form.kind());
if let Err(e) = self.client.submit_order(&order_form).await {

View File

@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::ops::Neg;
use dyn_clone::DynClone;
use log::info;
@ -8,7 +9,7 @@ use crate::BoxError;
use crate::connectors::Connector;
use crate::events::{ActionMessage, Event, EventKind, EventMetadata};
use crate::managers::OptionUpdate;
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, OrderMetadata, Position, PositionProfitState, TradingFees};
/***************
* DEFINITIONS
@ -21,12 +22,14 @@ pub trait PositionStrategy: DynClone + Send + Sync {
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
fn post_tick(
&mut self,
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
}
@ -68,7 +71,10 @@ impl Debug for dyn OrderStrategy {
#[derive(Clone, Debug)]
pub struct HiddenTrailingStop {
// position id: stop_percentage
stop_percentages: HashMap<u64, f64>,
// position_id: bool
stop_loss_flags: HashMap<u64, bool>,
capital_max_loss: f64,
capital_min_profit: f64,
capital_good_profit: f64,
@ -80,6 +86,7 @@ pub struct HiddenTrailingStop {
max_loss_percentage: f64,
}
impl HiddenTrailingStop {
fn update_stop_percentage(&mut self, position: &Position) {
if let Some(profit_state) = position.profit_state() {
@ -144,6 +151,7 @@ impl Default for HiddenTrailingStop {
HiddenTrailingStop {
stop_percentages: Default::default(),
stop_loss_flags: Default::default(),
capital_max_loss,
capital_min_profit,
capital_good_profit,
@ -168,9 +176,11 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position,
current_tick: u64,
positions_history: &HashMap<u64, Position>,
_: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let pl_perc = position.pl_perc();
// setting the state of the position based on its profit/loss percentage
let state = {
if pl_perc > self.good_profit_percentage {
PositionProfitState::Profit
@ -189,6 +199,8 @@ impl PositionStrategy for HiddenTrailingStop {
let event_metadata = EventMetadata::new(Some(position.id()), None);
let new_position = position.with_profit_state(Some(state));
// checking if there was a state change between the current position
// and its last state
match opt_prev_position {
Some(prev) => {
if prev.profit_state() == Some(state) {
@ -239,28 +251,75 @@ impl PositionStrategy for HiddenTrailingStop {
position: Position,
_: u64,
_: &HashMap<u64, Position>,
fees: &Vec<TradingFees>,
) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let close_message = ActionMessage::ClosePosition {
let taker_fee = fees
.iter()
.filter_map(|x| match x {
TradingFees::Taker {
platform,
percentage,
} if platform == &position.platform() => Some(percentage),
_ => None,
})
.next().map_or_else(|| 0.0, |&x| x);
let stop_loss_price = {
if position.is_short() {
position.base_price() * (1.0 - (self.max_loss_percentage - taker_fee) / 100.0)
} else {
position.base_price() * (1.0 + (self.max_loss_percentage - taker_fee) / 100.0)
}
};
let close_position_orders_msg = ActionMessage::ClosePositionOrders {
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 close_position_msg = ActionMessage::ClosePosition {
position_id: position.id(),
};
let set_stop_loss_msg = ActionMessage::SubmitOrder {
order: OrderForm::new(position.pair().clone(),
OrderKind::Stop { price: stop_loss_price },
position.platform(),
position.amount().neg())
.with_metadata(Some(OrderMetadata::with_position_id(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() {
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);
}
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);
}
// let's check if we surpassed an existing stop percentage
if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
if &position.pl_perc() <= existing_stop_percentage {
info!("Stop percentage surpassed. Closing position.");
return (position, None, Some(vec![close_message]));
messages.push(close_position_msg);
return (position, None, Some(messages));
}
}
self.update_stop_percentage(&position);
(position, None, None)
(position, None, Some(messages))
}
}
@ -531,7 +590,7 @@ pub struct MarketEnforce {
impl Default for MarketEnforce {
fn default() -> Self {
Self {
threshold: 1.0 / 15.0,
threshold: 100.0,
}
}
}