merge rust

This commit is contained in:
Giulio De Pasquale 2021-02-17 17:19:49 +00:00
commit b8f239ec31
7 changed files with 941 additions and 455 deletions

View File

@ -17,7 +17,7 @@ use tokio::time::Duration;
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, TradingPlatform, WalletKind, PositionState, PriceTicker, Trade, TradingFees, TradingPlatform, WalletKind,
}; };
use crate::BoxError; use crate::BoxError;
@ -57,21 +57,56 @@ impl Client {
pair: &SymbolPair, pair: &SymbolPair,
) -> Result<Option<Vec<Position>>, BoxError> { ) -> Result<Option<Vec<Position>>, BoxError> {
// retrieving open positions and order book to calculate effective profit/loss // 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.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()); 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 // updating positions with effective profit/loss
// TODO: change fee with account's taker fee
positions.iter_mut().flatten().for_each(|x| { 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() { if x.is_short() {
x.update_profit_loss(best_ask, 0.2); x.update_profit_loss(best_ask, *fee);
} else { } 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) .active_orders(pair)
.await? .await?
.into_iter() .into_iter()
.filter(|x| &x.symbol == pair) .filter(|x| &x.pair() == &pair)
.collect()) .collect())
} }
@ -153,6 +188,7 @@ pub trait Connector: Send + Sync {
&self, &self,
pair: &SymbolPair, pair: &SymbolPair,
) -> Result<Option<Vec<OrderDetails>>, BoxError>; ) -> Result<Option<Vec<OrderDetails>>, BoxError>;
async fn trading_fees(&self) -> Result<Vec<TradingFees>, BoxError>;
} }
impl Debug for dyn Connector { impl Debug for dyn Connector {
@ -176,7 +212,7 @@ impl BitfinexConnector {
if e.to_string().contains("nonce: small") { if e.to_string().contains("nonce: small") {
return RetryPolicy::WaitRetry(Duration::from_millis(1)); return RetryPolicy::WaitRetry(Duration::from_millis(1));
} }
return RetryPolicy::ForwardError(e); RetryPolicy::ForwardError(e)
} }
pub fn new(api_key: &str, api_secret: &str) -> Self { pub fn new(api_key: &str, api_secret: &str) -> Self {
@ -186,7 +222,9 @@ impl BitfinexConnector {
} }
fn format_trading_pair(pair: &SymbolPair) -> String { 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()) format!("{}:{}", pair.base(), pair.quote())
} else { } else {
format!("{}{}", pair.base(), pair.quote()) format!("{}{}", pair.base(), pair.quote())
@ -275,37 +313,48 @@ impl Connector for BitfinexConnector {
async fn submit_order(&self, order: &OrderForm) -> Result<ActiveOrder, BoxError> { async fn submit_order(&self, order: &OrderForm) -> Result<ActiveOrder, BoxError> {
let symbol_name = format!("t{}", BitfinexConnector::format_trading_pair(order.pair())); let symbol_name = format!("t{}", BitfinexConnector::format_trading_pair(order.pair()));
let amount = order.amount();
let order_form = match order.kind() { let order_form = {
OrderKind::Limit { price, amount } => { let pre_leverage = {
bitfinex::orders::OrderForm::new(symbol_name, price, amount, order.into()) 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 = let response =
BitfinexConnector::retry_nonce(|| self.bfx.orders.submit_order(&order_form)).await?; 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)) Ok((!mapped_vec.is_empty()).then_some(mapped_vec))
} }
async fn trading_fees(&self) -> Result<Vec<TradingFees>, 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 { 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())
} }
} }
@ -383,20 +476,18 @@ impl TryFrom<&bitfinex::responses::OrderResponse> for ActiveOrder {
type Error = BoxError; type Error = BoxError;
fn try_from(response: &OrderResponse) -> Result<Self, Self::Error> { fn try_from(response: &OrderResponse) -> Result<Self, Self::Error> {
Ok(Self { let pair = SymbolPair::from_str(response.symbol())?;
exchange: Exchange::Bitfinex,
id: response.id(), Ok(ActiveOrder::new(
group_id: response.gid(), Exchange::Bitfinex,
client_id: Some(response.cid()), response.id(),
symbol: SymbolPair::from_str(response.symbol())?, pair.clone(),
details: OrderForm::new( OrderForm::new(pair, response.into(), response.into(), response.amount()),
SymbolPair::from_str(response.symbol())?, response.mts_create(),
response.into(), response.mts_update(),
response.into(), )
), .with_group_id(response.gid())
creation_timestamp: 0, .with_client_id(Some(response.cid())))
update_timestamp: 0,
})
} }
} }
@ -412,6 +503,15 @@ impl TryInto<Position> 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( Ok(Position::new(
SymbolPair::from_str(self.symbol())?, SymbolPair::from_str(self.symbol())?,
state, state,
@ -421,6 +521,8 @@ impl TryInto<Position> for bitfinex::positions::Position {
self.pl_perc(), self.pl_perc(),
self.price_liq(), self.price_liq(),
self.position_id(), self.position_id(),
platform,
self.leverage(),
) )
.with_creation_date(self.mts_create()) .with_creation_date(self.mts_create())
.with_creation_update(self.mts_update())) .with_creation_update(self.mts_update()))
@ -439,7 +541,7 @@ impl From<&OrderForm> for bitfinex::orders::OrderKind {
OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::ExchangeFok, OrderKind::FillOrKill { .. } => bitfinex::orders::OrderKind::ExchangeFok,
OrderKind::ImmediateOrCancel { .. } => bitfinex::orders::OrderKind::ExchangeIoc, 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::Limit { .. } => bitfinex::orders::OrderKind::Limit,
OrderKind::Market { .. } => bitfinex::orders::OrderKind::Market, OrderKind::Market { .. } => bitfinex::orders::OrderKind::Market,
OrderKind::Stop { .. } => bitfinex::orders::OrderKind::Stop, 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 => { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => {
Self::Limit { Self::Limit {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => {
Self::Market { Self::Market
amount: response.amount(),
}
} }
bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => {
Self::Stop { Self::Stop {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(), price: response.price(),
amount: response.amount(),
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
| bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop {
distance: response.price_trailing().expect("Distance not found!"), distance: response.price_trailing().expect("Distance not found!"),
amount: response.amount(),
}, },
bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => {
Self::FillOrKill { Self::FillOrKill {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => {
Self::ImmediateOrCancel { Self::ImmediateOrCancel {
price: response.price(), 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 => { bitfinex::orders::OrderKind::Limit | bitfinex::orders::OrderKind::ExchangeLimit => {
Self::Limit { Self::Limit {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => { bitfinex::orders::OrderKind::Market | bitfinex::orders::OrderKind::ExchangeMarket => {
Self::Market { Self::Market {}
amount: response.amount(),
}
} }
bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => { bitfinex::orders::OrderKind::Stop | bitfinex::orders::OrderKind::ExchangeStop => {
Self::Stop { Self::Stop {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::StopLimit bitfinex::orders::OrderKind::StopLimit
| bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit { | bitfinex::orders::OrderKind::ExchangeStopLimit => Self::StopLimit {
price: response.price(), price: response.price(),
amount: response.amount(),
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
| bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop { | bitfinex::orders::OrderKind::ExchangeTrailingStop => Self::TrailingStop {
distance: response.price_trailing().expect("Distance not found!"), distance: response.price_trailing().expect("Distance not found!"),
amount: response.amount(),
}, },
bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => { bitfinex::orders::OrderKind::Fok | bitfinex::orders::OrderKind::ExchangeFok => {
Self::FillOrKill { Self::FillOrKill {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => { bitfinex::orders::OrderKind::Ioc | bitfinex::orders::OrderKind::ExchangeIoc => {
Self::ImmediateOrCancel { Self::ImmediateOrCancel {
price: response.price(), price: response.price(),
amount: response.amount(),
} }
} }
} }
@ -581,16 +667,16 @@ impl From<&bitfinex::orders::ActiveOrder> for ActiveOrder {
fn from(order: &bitfinex::orders::ActiveOrder) -> Self { fn from(order: &bitfinex::orders::ActiveOrder) -> Self {
let pair = SymbolPair::from_str(&order.symbol()).expect("Invalid symbol!"); let pair = SymbolPair::from_str(&order.symbol()).expect("Invalid symbol!");
Self { ActiveOrder::new(
exchange: Exchange::Bitfinex, Exchange::Bitfinex,
id: order.id(), order.id(),
group_id: order.group_id().map(|x| x as u64), pair.clone(),
client_id: Some(order.client_id()), OrderForm::new(pair, order.into(), order.into(), order.amount()),
symbol: pair.clone(), order.creation_timestamp(),
details: OrderForm::new(pair, order.into(), order.into()), order.update_timestamp(),
creation_timestamp: order.creation_timestamp(), )
update_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 // TODO: fields are hardcoded, to fix
impl From<&bitfinex::responses::TradeResponse> for Trade { impl From<&bitfinex::responses::TradeResponse> for Trade {
fn from(response: &TradeResponse) -> Self { fn from(response: &TradeResponse) -> Self {
let pair = SymbolPair::from_str(&response.symbol).unwrap(); let pair = SymbolPair::from_str(&response.symbol()).unwrap();
let fee = { let fee = {
if response.is_maker { if response.is_maker() {
OrderFee::Maker(response.fee) OrderFee::Maker(response.fee())
} else { } else {
OrderFee::Taker(response.fee) OrderFee::Taker(response.fee())
} }
}; };
Self { Self {
trade_id: response.trade_id, trade_id: response.trade_id(),
pair, pair,
execution_timestamp: response.execution_timestamp, execution_timestamp: response.execution_timestamp(),
price: response.execution_price, price: response.execution_price(),
amount: response.execution_amount, amount: response.execution_amount(),
fee, fee,
fee_currency: Symbol::new(response.symbol.clone()), fee_currency: Symbol::new(response.symbol().to_owned()),
} }
} }
} }

View File

@ -28,10 +28,17 @@ impl Symbol {
pub const LTC: Symbol = Symbol::new_static("LTC"); pub const LTC: Symbol = Symbol::new_static("LTC");
pub const DOT: Symbol = Symbol::new_static("DOT"); 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 // Paper trading
pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC");
pub const TESTUSD: Symbol = Symbol::new_static("TESTUSD"); 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 // Fiat coins
pub const USD: Symbol = Symbol::new_static("USD"); pub const USD: Symbol = Symbol::new_static("USD");
pub const GBP: Symbol = Symbol::new_static("GBP"); pub const GBP: Symbol = Symbol::new_static("GBP");

View File

@ -5,15 +5,16 @@ use crate::models::OrderForm;
#[derive(Debug)] #[derive(Debug)]
pub struct ActorMessage { pub struct ActorMessage {
pub(crate) message: Message, pub(crate) message: ActionMessage,
pub(crate) respond_to: oneshot::Sender<OptionUpdate>, pub(crate) respond_to: oneshot::Sender<OptionUpdate>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Message { pub enum ActionMessage {
Update { tick: u64 }, Update { tick: u64 },
ClosePosition { position_id: u64 }, ClosePosition { position_id: u64 },
SubmitOrder { order: OrderForm }, SubmitOrder { order: OrderForm },
ClosePositionOrders { position_id: u64 },
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]

View File

@ -1,6 +1,8 @@
#![feature(drain_filter)] #![feature(drain_filter)]
#![feature(bool_to_option)] #![feature(bool_to_option)]
use std::env;
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter::{Debug, Trace}; use log::LevelFilter::{Debug, Trace};
use tokio::time::Duration; use tokio::time::Duration;
@ -23,33 +25,27 @@ pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), BoxError> { async fn main() -> Result<(), BoxError> {
setup_logger()?; setup_logger()?;
dotenv::dotenv()?;
// TEST let api_key = env::vars()
let test_api_key = "P1EVE68DJByDAkGQvpIkTwfrbYXd2Vo2ZaIhTYb9vx2"; .find(|(k, v)| k == "API_KEY")
let test_api_secret = "1nicg8z0zKVEt5Rb7ZDpIYjVYVTgvCaCPMZqB0niFli"; .map(|(k, v)| v)
.ok_or("API_KEY not set!")?;
// REAL let api_secret = env::vars()
// let orders_api_key = "hc5nDvYbFYJZMKdnzYq8P4AzCSwjxfQHnMyrg69Sf4c"; .find(|(k, v)| k == "API_SECRET")
// let orders_api_secret = "53x9goIOpbOtBoPi7dmigK5Cq5e0282EUO2qRIMEXlh"; .map(|(k, v)| v)
// let prices_api_key = "gTfFZUCwRBE0Z9FZjyk9HNe4lZ7XuiZY9rrW71SyUr9"; .ok_or("API_SECRET not set!")?;
// let prices_api_secret = "zWbxvoFZad3BPIiXK4DKfEvC0YsAuaApbeAyI8OBXgN";
// let positions_api_key = "PfR7BadPZPNdVZnkHFBfAjsg7gjt8pAecMj5B8eRPFi";
// let positions_api_secret = "izzvxtE3XsBBRpVCHGJ8f60UA56SmPNbBvJGVd67aqD";
let bitfinex = ExchangeDetails::Bitfinex { let bitfinex = ExchangeDetails::Bitfinex {
prices_api_key: test_api_key.into(), api_key: api_key.into(),
prices_api_secret: test_api_secret.into(), api_secret: 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(),
}; };
let mut bot = BfxBot::new( let mut bot = BfxBot::new(
vec![bitfinex], vec![bitfinex],
vec![Symbol::BTC, Symbol::ETH, Symbol::XMR], vec![Symbol::DERIV_ETH, Symbol::DERIV_BTC],
Symbol::USD, Symbol::DERIV_USDT,
Duration::new(1, 0), Duration::new(10, 0),
); );
Ok(bot.start_loop().await?) Ok(bot.start_loop().await?)

View File

@ -12,14 +12,14 @@ use tokio::time::Duration;
use crate::connectors::{Client, ExchangeDetails}; use crate::connectors::{Client, ExchangeDetails};
use crate::currency::SymbolPair; use crate::currency::SymbolPair;
use crate::events::{ActorMessage, Event, Message}; use crate::events::{ActionMessage, ActorMessage, Event};
use crate::models::{ 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; use crate::BoxError;
pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<Message>>); pub type OptionUpdate = (Option<Vec<Event>>, Option<Vec<ActionMessage>>);
/****************** /******************
* PRICES * PRICES
@ -33,6 +33,48 @@ pub struct PriceManager {
client: Client, client: Client,
} }
impl PriceManager {
pub fn new(receiver: Receiver<ActorMessage>, 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<PriceEntry, BoxError> {
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 { pub struct PriceManagerHandle {
sender: Sender<ActorMessage>, sender: Sender<ActorMessage>,
} }
@ -58,7 +100,7 @@ impl PriceManagerHandle {
self.sender self.sender
.send(ActorMessage { .send(ActorMessage {
message: Message::Update { tick }, message: ActionMessage::Update { tick },
respond_to: send, respond_to: send,
}) })
.await?; .await?;
@ -67,51 +109,6 @@ impl PriceManagerHandle {
} }
} }
impl PriceManager {
pub fn new(receiver: Receiver<ActorMessage>, 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<PriceEntry, BoxError> {
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)] #[derive(Clone, Debug)]
pub struct PriceEntry { pub struct PriceEntry {
tick: u64, tick: u64,
@ -179,7 +176,7 @@ impl PositionManagerHandle {
self.sender self.sender
.send(ActorMessage { .send(ActorMessage {
message: Message::Update { tick }, message: ActionMessage::Update { tick },
respond_to: send, respond_to: send,
}) })
.await?; .await?;
@ -224,7 +221,7 @@ impl PositionManager {
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 {
Message::Update { tick } => self.update(tick).await?, ActionMessage::Update { tick } => self.update(tick).await?,
_ => (None, None), _ => (None, None),
}; };
@ -247,11 +244,7 @@ impl PositionManager {
Some(positions) => { Some(positions) => {
// checking if there are positions open for our pair // checking if there are positions open for our pair
match positions match positions.into_iter().find(|x| x.pair() == &self.pair) {
.into_iter()
.filter(|x| x.pair() == &self.pair)
.next()
{
// no open positions for our pair, setting active position to none // no open positions for our pair, setting active position to none
None => { None => {
self.active_position = None; self.active_position = None;
@ -300,10 +293,7 @@ impl PositionManager {
None => self.current_tick() - 1, None => self.current_tick() - 1,
}; };
self.positions_history self.positions_history.get(&tick).filter(|x| x.id() == id)
.get(&tick)
.filter(|x| x.id() == id)
.and_then(|x| Some(x))
} }
} }
@ -312,7 +302,7 @@ impl PositionManager {
******************/ ******************/
// Position ID: Order ID // Position ID: Order ID
pub type TrackedPositionsMap = HashMap<u64, u64>; pub type TrackedPositionsMap = HashMap<u64, Vec<u64>>;
pub struct OrderManagerHandle { pub struct OrderManagerHandle {
sender: Sender<ActorMessage>, sender: Sender<ActorMessage>,
@ -354,7 +344,36 @@ impl OrderManagerHandle {
self.sender self.sender
.send(ActorMessage { .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<OptionUpdate, BoxError> {
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<OptionUpdate, BoxError> {
let (send, recv) = oneshot::channel();
self.sender
.send(ActorMessage {
message: ActionMessage::SubmitOrder { order: order_form },
respond_to: send, respond_to: send,
}) })
.await?; .await?;
@ -391,9 +410,14 @@ impl OrderManager {
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 {
Message::Update { .. } => self.update().await?, ActionMessage::Update { .. } => self.update().await?,
Message::ClosePosition { position_id } => self.close_position(position_id).await?, ActionMessage::ClosePosition { position_id } => {
_ => (None, None), 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 Ok(msg
@ -402,6 +426,53 @@ impl OrderManager {
.map_err(|_| BoxError::from("Could not send message."))?) .map_err(|_| BoxError::from("Could not send message."))?)
} }
pub async fn close_position_orders(&self, position_id: u64) -> Result<OptionUpdate, BoxError> {
info!("Closing outstanding orders for position #{}", position_id);
if let Some(position_orders) = self.tracked_positions.get(&position_id) {
// retrieving open orders
let open_orders = self.client.active_orders(&self.pair).await?;
let position_orders: Vec<_> = position_orders
.iter()
.filter_map(|&x| open_orders.iter().find(|y| y.id() == x))
.collect();
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<OptionUpdate, BoxError> {
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<OptionUpdate, BoxError> { pub async fn close_position(&mut self, position_id: u64) -> Result<OptionUpdate, BoxError> {
info!("Closing position #{}", position_id); 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) { if let Some(position) = open_positions.into_iter().find(|x| x.id() == position_id) {
let opt_position_order = open_orders let opt_position_order = open_orders
.iter() .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. // checking if the position has an open order.
// If so, don't do anything since the order is taken care of // If so, don't do anything since the order is taken care of
// in the update phase. // in the update phase.
// If no order is open, send an undercut limit order at the best current price. // 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 // No open order, undercutting best price with limit order
let closing_price = self.best_closing_price(&position, &order_book); let closing_price = self.best_closing_price(&position, &order_book);
// TODO: hardcoded platform to Margin!
let order_form = OrderForm::new( let order_form = OrderForm::new(
self.pair.clone(), self.pair.clone(),
OrderKind::Limit { OrderKind::Limit {
price: closing_price, 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()); info!("Submitting {} order", order_form.kind());
if let Err(e) = self.client.submit_order(&order_form).await { if let Err(e) = self.client.submit_order(&order_form).await {
@ -458,8 +532,8 @@ impl OrderManager {
Ok((None, None)) Ok((None, None))
} }
pub async fn update(&self) -> Result<OptionUpdate, BoxError> { pub async fn update(&mut self) -> Result<OptionUpdate, BoxError> {
trace!("\t[OrderManager] Updating {}", self.pair); debug!("\t[OrderManager] Updating {}", self.pair);
let (res_open_orders, res_order_book) = tokio::join!( let (res_open_orders, res_order_book) = tokio::join!(
self.client.active_orders(&self.pair), self.client.active_orders(&self.pair),
@ -468,8 +542,49 @@ impl OrderManager {
let (open_orders, order_book) = (res_open_orders?, res_order_book?); 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 { for active_order in open_orders {
debug!( trace!(
"Found open order, calling \"{}\" strategy.", "Found open order, calling \"{}\" strategy.",
self.strategy.name() self.strategy.name()
); );
@ -479,9 +594,9 @@ impl OrderManager {
if let Some(messages) = strat_messages { if let Some(messages) = strat_messages {
for m in messages { for m in messages {
match m { match m {
Message::SubmitOrder { order: order_form } => { ActionMessage::SubmitOrder { order: order_form } => {
info!("Closing open order..."); 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?; self.client.cancel_order(&active_order).await?;
info!("\tSubmitting {}...", order_form.kind()); info!("\tSubmitting {}...", order_form.kind());
@ -508,26 +623,22 @@ impl OrderManager {
let delta = (ask - bid) / 10.0; let delta = (ask - bid) / 10.0;
let closing_price = { let closing_price = {
let closing_price = { if position.is_short() {
if position.is_short() { bid - delta
bid - delta
} else {
ask + delta
}
};
if avg > 9999.0 {
if position.is_short() {
closing_price.ceil()
} else {
closing_price.floor()
}
} else { } 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( order_manager: OrderManagerHandle::new(
pair.clone(), pair.clone(),
client.clone(), client.clone(),
Box::new(FastOrderStrategy::default()), Box::new(MarketEnforce::default()),
), ),
position_manager: PositionManagerHandle::new( position_manager: PositionManagerHandle::new(
pair.clone(), pair,
client.clone(), client,
Box::new(TrailingStop::new()), Box::new(HiddenTrailingStop::default()),
), ),
} }
} }
@ -578,10 +689,18 @@ impl PairManager {
if let Some(messages) = messages { if let Some(messages) = messages {
for m in messages { for m in messages {
match m { match m {
Message::ClosePosition { position_id } => { ActionMessage::Update { .. } => {}
ActionMessage::ClosePosition { position_id } => {
self.order_manager.close_position(position_id).await?; 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 { pub struct ExchangeManager {
kind: ExchangeDetails, kind: ExchangeDetails,
pair_managers: Vec<PairManager>, pair_managers: Vec<PairManager>,
client: Client,
} }
impl ExchangeManager { impl ExchangeManager {
pub fn new(kind: &ExchangeDetails, pairs: &Vec<SymbolPair>) -> Self { pub fn new(kind: &ExchangeDetails, pairs: &[SymbolPair]) -> Self {
let client = Client::new(kind); let client = Client::new(kind);
let pair_managers = pairs let pair_managers = pairs
.into_iter() .iter()
.map(|x| PairManager::new(x.clone(), client.clone())) .map(|x| PairManager::new(x.clone(), client.clone()))
.collect(); .collect();
Self { Self {
kind: kind.clone(), kind: kind.clone(),
pair_managers, pair_managers,
client,
} }
} }
@ -619,7 +736,7 @@ impl ExchangeManager {
.collect(); .collect();
// execute the futures // execute the futures
while let Some(_) = futures.next().await {} while futures.next().await.is_some() {}
Ok(()) Ok(())
} }

View File

@ -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 { pub fn id(&self) -> u64 {
self.id self.id
} }
@ -162,14 +150,71 @@ impl OrderDetails {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ActiveOrder { pub struct ActiveOrder {
pub(crate) exchange: Exchange, exchange: Exchange,
pub(crate) id: u64, id: u64,
pub(crate) group_id: Option<u64>, group_id: Option<u64>,
pub(crate) client_id: Option<u64>, client_id: Option<u64>,
pub(crate) symbol: SymbolPair, pair: SymbolPair,
pub(crate) details: OrderForm, order_form: OrderForm,
pub(crate) creation_timestamp: u64, creation_timestamp: u64,
pub(crate) update_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<u64>) -> Self {
self.group_id = group_id;
self
}
pub fn with_client_id(mut self, client_id: Option<u64>) -> 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<u64> {
self.group_id
}
pub fn client_id(&self) -> Option<u64> {
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 { impl Hash for ActiveOrder {
@ -186,7 +231,7 @@ impl PartialEq for ActiveOrder {
impl Eq for ActiveOrder {} impl Eq for ActiveOrder {}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum TradingPlatform { pub enum TradingPlatform {
Exchange, Exchange,
Derivative, Derivative,
@ -213,34 +258,13 @@ impl Display for TradingPlatform {
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum OrderKind { pub enum OrderKind {
Limit { Limit { price: f64 },
price: f64, Market,
amount: f64, Stop { price: f64 },
}, StopLimit { price: f64, limit_price: f64 },
Market { TrailingStop { distance: f64 },
amount: f64, FillOrKill { price: f64 },
}, ImmediateOrCancel { price: 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,
},
} }
impl OrderKind { impl OrderKind {
@ -260,67 +284,32 @@ impl OrderKind {
impl Display for OrderKind { 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, 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!( write!(
f, f,
"[{} | Price: {:0.5}, Amount: {:0.5}]", "[{} | Price: {:0.5}, Limit Price: {:0.5}]",
self.as_str(), self.as_str(),
price, 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 limit_price
) )
} }
OrderKind::TrailingStop { distance, amount } => { OrderKind::TrailingStop { distance } => {
write!( write!(f, "[{} | Distance: {:0.5}]", self.as_str(), distance,)
f,
"[{} | Distance: {:0.5}, Amount: {:0.5}]",
self.as_str(),
distance,
amount
)
} }
OrderKind::FillOrKill { price, amount } => { OrderKind::FillOrKill { price } => {
write!( write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
f,
"[{} | Price: {:0.5}, Amount: {:0.5}]",
self.as_str(),
price,
amount
)
} }
OrderKind::ImmediateOrCancel { price, amount } => { OrderKind::ImmediateOrCancel { price } => {
write!( write!(f, "[{} | Price: {:0.5}]", self.as_str(), price,)
f,
"[{} | Price: {:0.5}, Amount: {:0.5}]",
self.as_str(),
price,
amount
)
} }
} }
} }
@ -331,17 +320,38 @@ pub struct OrderForm {
pair: SymbolPair, pair: SymbolPair,
kind: OrderKind, kind: OrderKind,
platform: TradingPlatform, platform: TradingPlatform,
amount: f64,
leverage: Option<f64>,
metadata: Option<OrderMetadata>,
} }
impl OrderForm { 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 { Self {
pair, pair,
kind: order_kind, kind: order_kind,
platform, platform,
amount,
leverage: None,
metadata: None,
} }
} }
pub fn with_leverage(mut self, leverage: Option<f64>) -> Self {
self.leverage = leverage;
self
}
pub fn with_metadata(mut self, metadata: Option<OrderMetadata>) -> Self {
self.metadata = metadata;
self
}
pub fn pair(&self) -> &SymbolPair { pub fn pair(&self) -> &SymbolPair {
&self.pair &self.pair
} }
@ -355,15 +365,7 @@ impl OrderForm {
} }
pub fn amount(&self) -> f64 { pub fn amount(&self) -> f64 {
match self.kind { self.amount
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,
}
} }
pub fn price(&self) -> Option<f64> { pub fn price(&self) -> Option<f64> {
@ -377,6 +379,31 @@ impl OrderForm {
OrderKind::ImmediateOrCancel { price, .. } => Some(price), OrderKind::ImmediateOrCancel { price, .. } => Some(price),
} }
} }
pub fn leverage(&self) -> Option<f64> {
self.leverage
}
pub fn metadata(&self) -> &Option<OrderMetadata> {
&self.metadata
}
}
#[derive(Debug, Clone)]
pub struct OrderMetadata {
position_id: Option<u64>,
}
impl OrderMetadata {
pub fn with_position_id(position_id: u64) -> Self {
OrderMetadata {
position_id: Some(position_id),
}
}
pub fn position_id(&self) -> Option<u64> {
self.position_id
}
} }
/*************** /***************
@ -396,6 +423,8 @@ pub struct Position {
position_id: u64, position_id: u64,
creation_date: Option<u64>, creation_date: Option<u64>,
creation_update: Option<u64>, creation_update: Option<u64>,
platform: TradingPlatform,
leverage: f64,
} }
impl Position { impl Position {
@ -408,6 +437,8 @@ impl Position {
pl_perc: f64, pl_perc: f64,
price_liq: f64, price_liq: f64,
position_id: u64, position_id: u64,
platform: TradingPlatform,
leverage: f64,
) -> Self { ) -> Self {
Position { Position {
pair, pair,
@ -421,6 +452,8 @@ impl Position {
creation_date: None, creation_date: None,
creation_update: None, creation_update: None,
profit_state: None, profit_state: None,
platform,
leverage,
} }
} }
@ -505,6 +538,12 @@ impl Position {
pub fn is_long(&self) -> bool { pub fn is_long(&self) -> bool {
self.amount.is_sign_positive() self.amount.is_sign_positive()
} }
pub fn platform(&self) -> TradingPlatform {
self.platform
}
pub fn leverage(&self) -> f64 {
self.leverage
}
} }
impl Hash for Position { impl Hash for Position {
@ -530,17 +569,6 @@ pub enum PositionProfitState {
Profit, 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)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum PositionState { pub enum PositionState {
Closed, Closed,
@ -563,3 +591,15 @@ pub struct Trade {
pub fee: OrderFee, pub fee: OrderFee,
pub fee_currency: Symbol, pub fee_currency: Symbol,
} }
#[derive(Debug)]
pub enum TradingFees {
Maker {
platform: TradingPlatform,
percentage: f64,
},
Taker {
platform: TradingPlatform,
percentage: f64,
},
}

View File

@ -4,12 +4,10 @@ use std::fmt::{Debug, Formatter};
use dyn_clone::DynClone; use dyn_clone::DynClone;
use log::info; 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::managers::OptionUpdate;
use crate::models::{ use crate::models::{ActiveOrder, OrderBook, OrderForm, OrderKind, Position, PositionProfitState};
ActiveOrder, OrderBook, OrderBookEntry, OrderForm, OrderKind, Position, PositionProfitState,
PositionState, TradingPlatform,
};
use crate::BoxError; use crate::BoxError;
/*************** /***************
@ -23,13 +21,13 @@ 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>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>); ) -> (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>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>); ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>);
} }
impl Debug for dyn PositionStrategy { impl Debug for dyn PositionStrategy {
@ -69,54 +67,54 @@ impl Debug for dyn OrderStrategy {
***************/ ***************/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TrailingStop { pub struct HiddenTrailingStop {
stop_percentages: HashMap<u64, f64>, 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 { impl HiddenTrailingStop {
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(),
}
}
fn update_stop_percentage(&mut self, position: &Position) { fn update_stop_percentage(&mut self, position: &Position) {
if let Some(profit_state) = position.profit_state() { if let Some(profit_state) = position.profit_state() {
let profit_state_delta = match profit_state { let profit_state_delta = match profit_state {
PositionProfitState::MinimumProfit => Some(0.2), PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta),
PositionProfitState::Profit => Some(0.1), PositionProfitState::Profit => Some(self.good_profit_trailing_delta),
_ => None, _ => None,
}; };
if let Some(profit_state_delta) = profit_state_delta { if let Some(profit_state_delta) = profit_state_delta {
let current_stop_percentage = position.pl_perc() - profit_state_delta; let current_stop_percentage = position.pl_perc() - profit_state_delta;
match profit_state { if let PositionProfitState::MinimumProfit | PositionProfitState::Profit =
PositionProfitState::MinimumProfit | PositionProfitState::Profit => { profit_state
match self.stop_percentages.get(&position.id()) { {
None => { match self.stop_percentages.get(&position.id()) {
None => {
self.stop_percentages
.insert(position.id(), current_stop_percentage);
}
Some(existing_threshold) => {
if existing_threshold < &current_stop_percentage {
self.stop_percentages self.stop_percentages
.insert(position.id(), current_stop_percentage); .insert(position.id(), current_stop_percentage);
} }
Some(existing_threshold) => {
if existing_threshold < &current_stop_percentage {
self.stop_percentages
.insert(position.id(), current_stop_percentage);
}
}
} }
} }
_ => {}
} }
} }
info!( info!(
"\tState: {:?} | PL%: {: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.pair().quote(),
position.pl_perc(), position.pl_perc(),
self.stop_percentages.get(&position.id()).unwrap_or(&0.0) 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 { 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
@ -135,19 +167,17 @@ impl PositionStrategy for TrailingStop {
position: Position, position: Position,
current_tick: u64, current_tick: u64,
positions_history: &HashMap<u64, Position>, positions_history: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let pl_perc = position.pl_perc(); let pl_perc = position.pl_perc();
let state = { let state = {
if pl_perc > TrailingStop::GOOD_PROFIT_PERC { if pl_perc > self.good_profit_percentage {
PositionProfitState::Profit PositionProfitState::Profit
} else if TrailingStop::MIN_PROFIT_PERC <= pl_perc } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
&& pl_perc < TrailingStop::GOOD_PROFIT_PERC
{
PositionProfitState::MinimumProfit 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 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 PositionProfitState::Loss
} else { } else {
PositionProfitState::Critical PositionProfitState::Critical
@ -156,7 +186,7 @@ impl PositionStrategy for TrailingStop {
let opt_prev_position = positions_history.get(&(current_tick - 1)); let opt_prev_position = positions_history.get(&(current_tick - 1));
let event_metadata = EventMetadata::new(Some(position.id()), None); 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 { match opt_prev_position {
Some(prev) => { Some(prev) => {
@ -205,7 +235,7 @@ impl PositionStrategy for TrailingStop {
events events
}; };
return (new_position, Some(events), None); (new_position, Some(events), None)
} }
fn post_tick( fn post_tick(
@ -213,20 +243,15 @@ impl PositionStrategy for TrailingStop {
position: Position, position: Position,
_: u64, _: u64,
_: &HashMap<u64, Position>, _: &HashMap<u64, Position>,
) -> (Position, Option<Vec<Event>>, Option<Vec<Message>>) { ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
let close_message = Message::ClosePosition { let close_message = ActionMessage::ClosePosition {
position_id: position.id(), position_id: position.id(),
}; };
// if critical, early return with close position // if critical, early return with close position
if let Some(profit_state) = position.profit_state() { if let Some(PositionProfitState::Critical) = position.profit_state() {
match profit_state { info!("Maximum loss reached. Closing position.");
PositionProfitState::Critical => { return (position, None, Some(vec![close_message]));
info!("Maximum loss reached. Closing position.");
return (position, None, Some(vec![close_message]));
}
_ => {}
}
}; };
// let's check if we surpassed an existing stop percentage // 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<u64, f64>,
// capital_max_loss: f64,
// capital_min_profit: f64,
// capital_good_profit: f64,
// min_profit_trailing_delta: f64,
// good_profit_trailing_delta: f64,
// leverage: f64,
// min_profit_percentage: f64,
// good_profit_percentage: f64,
// max_loss_percentage: f64,
// }
//
// impl TrailingStop {
// fn update_stop_percentage(&mut self, position: &Position) -> Option<OrderForm> {
// let mut order_form = None;
//
// if let Some(profit_state) = position.profit_state() {
// let profit_state_delta = match profit_state {
// PositionProfitState::MinimumProfit => Some(self.min_profit_trailing_delta),
// PositionProfitState::Profit => Some(self.good_profit_trailing_delta),
// _ => None,
// };
//
// if let Some(profit_state_delta) = profit_state_delta {
// let current_stop_percentage = position.pl_perc() - profit_state_delta;
// let price_percentage_delta = {
// if position.is_short() {
// // 1.0 is the base price
// 1.0 - current_stop_percentage / 100.0
// } else {
// 1.0 + current_stop_percentage / 100.0
// }
// };
//
// println!("Delta: {}", price_percentage_delta);
//
// if let PositionProfitState::MinimumProfit | PositionProfitState::Profit =
// profit_state
// {
// match self.stop_percentages.get(&position.id()) {
// None => {
// self.stop_percentages
// .insert(position.id(), current_stop_percentage);
//
// trace!("Setting trailing stop, asking order manager to cancel previous orders.");
// order_form = Some(
// OrderForm::new(
// position.pair().clone(),
// OrderKind::Limit {
// price: position.base_price() * price_percentage_delta,
// },
// position.platform(),
// position.amount().neg(),
// )
// .with_metadata(OrderMetadata::with_position_id(position.id())),
// );
// }
// Some(existing_threshold) => {
// // follow and update trailing stop
// if existing_threshold < &current_stop_percentage {
// self.stop_percentages
// .insert(position.id(), current_stop_percentage);
//
// trace!("Updating threshold, asking order manager to cancel previous orders.");
// order_form = Some(
// OrderForm::new(
// position.pair().clone(),
// OrderKind::Limit {
// price: position.base_price() * price_percentage_delta,
// },
// position.platform(),
// position.amount().neg(),
// )
// .with_metadata(OrderMetadata::with_position_id(position.id())),
// );
// }
// }
// }
// }
// }
//
// info!(
// "\tState: {:?} | PL: {:0.2}{} ({:0.2}%) | Stop: {:0.2}",
// position.profit_state().unwrap(),
// position.pl(),
// position.pair().quote(),
// position.pl_perc(),
// self.stop_percentages.get(&position.id()).unwrap_or(&0.0)
// );
// }
//
// order_form
// }
// }
//
// impl Default for TrailingStop {
// fn default() -> Self {
// let leverage = 5.0;
//
// // in percentage
// let capital_max_loss = 15.0;
// let capital_min_profit = 1.0;
// let capital_good_profit = 6.0;
//
// let weighted_min_profit = capital_min_profit / leverage;
// let weighted_good_profit = capital_good_profit / leverage;
// let weighted_max_loss = capital_max_loss / leverage;
//
// let min_profit_trailing_delta = weighted_min_profit * 0.2;
// let good_profit_trailing_delta = weighted_good_profit * 0.08;
//
// let min_profit_percentage = weighted_min_profit + min_profit_trailing_delta;
// let good_profit_percentage = weighted_good_profit + good_profit_trailing_delta;
// let max_loss_percentage = -weighted_max_loss;
//
// TrailingStop {
// stop_percentages: Default::default(),
// capital_max_loss,
// capital_min_profit,
// capital_good_profit,
// min_profit_trailing_delta,
// good_profit_trailing_delta,
// leverage,
// min_profit_percentage,
// good_profit_percentage,
// max_loss_percentage,
// }
// }
// }
// impl PositionStrategy for TrailingStop {
// fn name(&self) -> String {
// "Trailing Stop".into()
// }
//
// /// Sets the profit state of an open position
// fn on_tick(
// &mut self,
// position: Position,
// current_tick: u64,
// positions_history: &HashMap<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// let pl_perc = position.pl_perc();
//
// let state = {
// if pl_perc > self.good_profit_percentage {
// PositionProfitState::Profit
// } else if (self.min_profit_percentage..self.good_profit_percentage).contains(&pl_perc) {
// PositionProfitState::MinimumProfit
// } else if (0.0..self.min_profit_percentage).contains(&pl_perc) {
// PositionProfitState::BreakEven
// } else if (self.max_loss_percentage..0.0).contains(&pl_perc) {
// PositionProfitState::Loss
// } else {
// PositionProfitState::Critical
// }
// };
//
// let opt_prev_position = positions_history.get(&(current_tick - 1));
// let event_metadata = EventMetadata::new(Some(position.id()), None);
// let new_position = position.with_profit_state(Some(state));
//
// match opt_prev_position {
// Some(prev) => {
// if prev.profit_state() == Some(state) {
// return (new_position, None, None);
// }
// }
// None => return (new_position, None, None),
// };
//
// let events = {
// let mut events = vec![];
//
// if state == PositionProfitState::Profit {
// events.push(Event::new(
// EventKind::ReachedGoodProfit,
// current_tick,
// Some(event_metadata),
// ));
// } else if state == PositionProfitState::MinimumProfit {
// events.push(Event::new(
// EventKind::ReachedMinProfit,
// current_tick,
// Some(event_metadata),
// ));
// } else if state == PositionProfitState::BreakEven {
// events.push(Event::new(
// EventKind::ReachedBreakEven,
// current_tick,
// Some(event_metadata),
// ));
// } else if state == PositionProfitState::Loss {
// events.push(Event::new(
// EventKind::ReachedLoss,
// current_tick,
// Some(event_metadata),
// ));
// } else {
// events.push(Event::new(
// EventKind::ReachedMaxLoss,
// current_tick,
// Some(event_metadata),
// ));
// }
//
// events
// };
//
// (new_position, Some(events), None)
// }
//
// fn post_tick(
// &mut self,
// position: Position,
// _: u64,
// _: &HashMap<u64, Position>,
// ) -> (Position, Option<Vec<Event>>, Option<Vec<ActionMessage>>) {
// let close_message = ActionMessage::ClosePosition {
// position_id: position.id(),
// };
//
// // if critical, early return with close position
// if let Some(PositionProfitState::Critical) = position.profit_state() {
// info!("Maximum loss reached. Closing position.");
// return (position, None, Some(vec![close_message]));
// };
//
// // let's check if we surpassed an existing stop percentage
// if let Some(existing_stop_percentage) = self.stop_percentages.get(&position.id()) {
// if &position.pl_perc() <= existing_stop_percentage {
// info!("Stop percentage surpassed. Closing position.");
// return (position, None, Some(vec![close_message]));
// }
// }
//
// // updated or new trailing stop. should cancel orders and submit new one
// if let Some(order_form) = self.update_stop_percentage(&position) {
// let mut messages = vec![];
//
// messages.push(ActionMessage::ClosePositionOrders {
// position_id: position.id(),
// });
// messages.push(ActionMessage::SubmitOrder { order: order_form });
//
// return (position, None, Some(messages));
// }
//
// (position, None, None)
// }
// }
/*
* ORDER STRATEGIES
*/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct FastOrderStrategy { pub struct MarketEnforce {
// threshold (%) for which we trigger a market order // threshold (%) for which we trigger a market order
// to close an open position // to close an open position
threshold: f64, threshold: f64,
} }
impl Default for FastOrderStrategy { impl Default for MarketEnforce {
fn default() -> Self { fn default() -> Self {
Self { threshold: 0.2 } Self {
threshold: 1.0 / 15.0,
}
} }
} }
impl FastOrderStrategy { impl OrderStrategy for MarketEnforce {
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
}
impl OrderStrategy for FastOrderStrategy {
fn name(&self) -> String { fn name(&self) -> String {
"Fast order strategy".into() "Market Enforce".into()
} }
fn on_open_order( fn on_open_order(
@ -276,7 +554,7 @@ impl OrderStrategy for FastOrderStrategy {
// long // long
let offer_comparison = { let offer_comparison = {
if order.details.amount() > 0.0 { 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()
@ -286,63 +564,24 @@ impl OrderStrategy for FastOrderStrategy {
// if the best offer is higher than our threshold, // if the best offer is higher than our threshold,
// ask the manager to close the position with a market order // ask the manager to close the position with a market order
let order_price = order let order_price = order
.details .order_form()
.price() .price()
.ok_or("The active order does not have a price!")?; .ok_or("The active order does not have a price!")?;
let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0; let delta = (1.0 - (offer_comparison / order_price)).abs() * 100.0;
if delta > self.threshold { if delta > self.threshold {
messages.push(Message::SubmitOrder { messages.push(ActionMessage::SubmitOrder {
order: OrderForm::new( order: OrderForm::new(
order.symbol.clone(), order.pair().clone(),
OrderKind::Market { OrderKind::Market,
amount: order.details.amount(), *order.order_form().platform(),
}, order.order_form().amount(),
order.details.platform().clone(), )
), .with_leverage(order.order_form().leverage())
.with_metadata(order.order_form().metadata().clone()),
}) })
} }
Ok((None, (!messages.is_empty()).then_some(messages))) Ok((None, (!messages.is_empty()).then_some(messages)))
} }
// fn on_position_order(
// &self,
// order: &ActiveOrder,
// _: &Position,
// order_book: &OrderBook,
// ) -> Result<OptionUpdate, BoxError> {
// 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)))
// }
} }