use crate::config::Configuration;
use crate::depth_cache::DepthCache;
use crate::telegram::TelegramBot;
use crate::trading_pair::TradingPair;
use crate::triangular_relationship::TriangularRelationship;
use binance::account::*;
use binance::api::*;
use binance::model::*;
use console::style;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct CalculationCluster {
relationships: HashMap<String, TriangularRelationship>,
depth_cache: DepthCache,
config: Configuration,
account: Account,
bot: TelegramBot,
}
impl CalculationCluster {
pub fn new(
relationships: HashMap<String, TriangularRelationship>,
depth_cache: DepthCache,
config: Configuration,
) -> CalculationCluster {
let config_clone = config.clone();
let account: Account = Binance::new(Some(config_clone.api_key), Some(config_clone.api_secret));
let config_clone = config.clone();
let bot: TelegramBot = TelegramBot::new(config_clone);
if config.telegram_enabled {
bot.start();
}
CalculationCluster {
relationships,
depth_cache,
config,
account,
bot,
}
}
pub fn start(&self) {
let mut execution_count = 0;
let relationships = self.relationships.clone();
let relationships_names: Vec<String> = self.relationships.keys().cloned().collect();
while execution_count < self.config.trading_execution_cap
|| self.config.trading_execution_cap == -1
{
relationships_names.iter().for_each(|rel| {
let deal = self.calculate_relationship(relationships.get(rel).unwrap().clone());
if (deal.get_profit() >= (self.config.trading_profit_threshold / 100.0))
&& ((self.get_epoch_ms() - deal.get_timestamp()) <= self.config.trading_age_threshold)
{
println!(
"[{}] Deal: {:?}...",
style(format!("{:+.3}%", deal.get_profit() * 100.0))
.bold()
.dim(),
deal.get_actions()
);
if self.config.telegram_enabled {
self.bot.send_message(format!(
"[{:+.3}%] Deal: {:?}...",
deal.get_profit() * 100.0,
deal.get_actions()
));
}
if self.config.trading_enabled {
self.execute_deal(deal);
self.bot.send_message("Deal executed.".to_string());
execution_count += 1;
} else {
println!(
"[{}] Trading is not enabled, skipping...",
style("INFO").bold().dim()
);
if self.config.telegram_enabled {
self
.bot
.send_message("[INFO] Trading is not enabled, skipping...".to_string())
}
}
}
})
}
}
fn get_epoch_ms(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
fn correct_quantity(&self, quantity: f64, step: f64) -> f64 {
(quantity / step).floor() * step
}
fn custom_round(&self, quantity: f64, step: f64) -> f64 {
let mut step = step;
let mut power: usize = 0;
while step < 1.0 {
step *= 10.0;
power += 1;
}
format!("{:.1$}", quantity, power).parse().unwrap()
}
fn calculate_relationship(&self, relationship: TriangularRelationship) -> Deal {
let pairs = relationship.get_trading_pairs();
let pair_names = relationship.get_pairs();
let pair_actions = relationship.get_actions();
let fee_multiplier = ((100.0 - self.config.trading_taker_fee) / 100.0).powi(3);
let mut lowest_timestamp: u64 = u64::MAX;
let mut timestamp: u64;
let mut profit: f64;
let mut best_profit: f64 = -1.0;
let mut current_quantity: f64;
let mut helper_quantity: f64;
let mut tmp_quantity: f64;
let mut results = Deal::new();
let mut tmp_deal: Deal;
let min_investment = (self.config.investment_min / self.config.investment_step) as i32;
let max_investment = (self.config.investment_max / self.config.investment_step) as i32;
for investment in (min_investment..=max_investment).step_by(1) {
let true_investment = investment as f64 * self.config.investment_step;
current_quantity = true_investment;
tmp_deal = Deal::new();
for (j, pair_name) in pair_names.iter().enumerate() {
let depth_book = self.depth_cache.get_depth(&pair_name);
timestamp = depth_book.event_time;
if timestamp < lowest_timestamp {
lowest_timestamp = timestamp;
}
helper_quantity = current_quantity;
current_quantity = 0.0;
if pair_actions[j] == "BUY" {
let prices = depth_book.asks;
for ask in prices.iter() {
tmp_quantity = self.correct_quantity(helper_quantity / ask.price, pairs[j].get_step());
if ask.qty >= tmp_quantity {
current_quantity += tmp_quantity;
} else {
tmp_quantity = self.correct_quantity(ask.qty, pairs[j].get_step());
current_quantity += tmp_quantity;
}
helper_quantity -= ask.qty * ask.price;
if helper_quantity <= 0.0 {
break;
}
}
tmp_deal.add_action(pairs[j].clone(), pair_actions[j].clone(), current_quantity)
} else {
tmp_deal.add_action(
pairs[j].clone(),
pair_actions[j].clone(),
self.correct_quantity(helper_quantity, pairs[j].get_step()),
);
let prices = depth_book.bids;
for bid in prices.iter() {
if bid.qty >= helper_quantity {
current_quantity +=
self.correct_quantity(helper_quantity, pairs[j].get_step()) * bid.price;
} else {
current_quantity += self.correct_quantity(bid.qty, pairs[j].get_step()) * bid.price;
}
helper_quantity -= bid.qty;
if helper_quantity <= 0.0 {
break;
}
}
}
}
profit = ((current_quantity * fee_multiplier) - true_investment) / true_investment;
if profit >= best_profit {
results = tmp_deal;
best_profit = profit;
}
}
results.set_profit(best_profit);
results.set_timestamp(lowest_timestamp);
results
}
fn execute_deal(&self, deal: Deal) {
let actions = deal.get_actions();
let total_actions = actions.len();
for (i, action) in actions.iter().enumerate() {
let buy_sell = action.get_action();
let trading_pair = action.get_pair();
let pair = trading_pair.get_symbol();
let qty = self.custom_round(action.get_quantity(), trading_pair.get_step());
let order: Transaction;
if buy_sell == "BUY" {
println!(
"[{}] Buying {} from symbol {}",
style(format!("{}/{}", i + 1, total_actions)).bold().dim(),
qty,
pair,
);
order = match self.account.market_buy(pair.clone(), qty) {
Ok(transaction) => transaction,
Err(e) => panic!(
"Failed to execute action #{} (symbol={}, qty={}): {}",
i + 1,
pair,
qty,
e
),
};
}
else if buy_sell == "SELL" {
println!(
"[{}] Selling {} from symbol {}",
style(format!("{}/{}", i + 1, total_actions)).bold().dim(),
qty,
pair,
);
order = match self.account.market_sell(pair.clone(), qty) {
Ok(transaction) => transaction,
Err(e) => panic!(
"Failed to execute action #{} (symbol={}, qty={}): {}",
i + 1,
pair,
qty,
e
),
};
}
else {
panic!("Unknown operation for action #{}: {}", i + 1, buy_sell);
}
let mut status: String = String::from("");
while status != "FILLED" {
status = match self.account.order_status(pair.clone(), order.order_id) {
Ok(v) => {
println!(
"[{}] {:?}",
style(format!("{}/{}", i + 1, total_actions)).bold().dim(),
v,
);
v.status
}
Err(e) => {
println!(
"[{}] Couldn't find order yet, will retry. Error: {}",
style(format!("{}/{}", i + 1, total_actions)).bold().dim(),
e,
);
String::from("")
}
}
}
}
println!(
"[{}] Successfully executed deal!",
style("INFO").bold().dim(),
);
}
}
#[derive(Debug, Clone)]
struct Deal {
profit: f64,
timestamp: u64,
actions: Vec<Action>,
}
impl Deal {
pub fn new() -> Deal {
Deal {
profit: -1.0,
timestamp: 0,
actions: Vec::new(),
}
}
pub fn add_action(&mut self, pair: TradingPair, action: String, quantity: f64) {
self.actions.push(Action::new(pair, action, quantity))
}
pub fn get_actions(&self) -> Vec<Action> {
self.actions.clone()
}
pub fn set_profit(&mut self, profit: f64) {
self.profit = profit
}
pub fn get_profit(&self) -> f64 {
self.profit
}
pub fn set_timestamp(&mut self, timestamp: u64) {
self.timestamp = timestamp
}
pub fn get_timestamp(&self) -> u64 {
self.timestamp
}
}
#[derive(Debug, Clone)]
struct Action {
pair: TradingPair,
action: String,
quantity: f64,
}
impl Action {
pub fn new(pair: TradingPair, action: String, quantity: f64) -> Action {
Action {
pair,
action,
quantity,
}
}
pub fn get_pair(&self) -> TradingPair {
self.pair.clone()
}
pub fn get_action(&self) -> String {
self.action.clone()
}
pub fn get_quantity(&self) -> f64 {
self.quantity
}
}