use crate::event::{chance::ChanceEvent, orderbook::OrderbookEvent};
use crate::model::chance::{ActionInfo, TriangularArbitrageChance};
use crate::model::order::OrderSide;
use crate::model::orderbook::{FullOrderbook, Orderbook};
use crate::model::symbol::SymbolInfo;
use crate::strings::split_symbol;
use num_traits::pow;
use ordered_float::OrderedFloat;
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast::{Receiver, Sender};
pub async fn task_pub_chance_all_taker_btc_usd(
mut receiver: Receiver<OrderbookEvent>,
_sender: Sender<ChanceEvent>,
local_full_orderbook: Arc<Mutex<FullOrderbook>>,
symbol_map: Arc<Mutex<BTreeMap<String, SymbolInfo>>>,
) -> Result<(), kucoin_api::failure::Error> {
let btc = String::from("BTC");
let usd = String::from("USDT");
let btc_usd = std::format!("{btc}-{usd}");
let usd_budget = 10.0;
loop {
let event = receiver.recv().await?;
let eth: Option<String>;
match event {
OrderbookEvent::OrderbookChangeReceived((symbol, _delta)) => {
if symbol == btc_usd {
continue;
}
let (coin, _) = split_symbol(symbol).unwrap();
eth = Some(coin);
}
_ => {
log::error!("Unrecognised event {event:?}");
continue;
}
}
let eth = eth.unwrap();
let eth_btc = std::format!("{eth}-{btc}");
let eth_usd = std::format!("{eth}-{usd}");
let full_orderbook = local_full_orderbook.lock().unwrap();
let orderbook_btc_usd = (*full_orderbook).get(&btc_usd);
let orderbook_eth_btc = (*full_orderbook).get(ð_btc);
let orderbook_eth_usd = (*full_orderbook).get(ð_usd);
if orderbook_btc_usd.is_none() || orderbook_eth_btc.is_none() || orderbook_eth_usd.is_none()
{
log::warn!("empty orderbook");
continue;
}
let mut chance = triangular_chance_sequence(
&btc_usd,
ð_btc,
ð_usd,
orderbook_btc_usd.unwrap(),
orderbook_eth_btc.unwrap(),
orderbook_eth_usd.unwrap(),
symbol_map.clone(),
usd_budget,
);
if chance.actions[1].action.eq(&OrderSide::Buy) {
chance.actions[0].ticker = btc_usd.clone();
chance.actions[1].ticker = eth_btc.clone();
chance.actions[2].ticker = eth_usd.clone();
} else {
chance.actions[0].ticker = eth_usd.clone();
chance.actions[1].ticker = eth_btc.clone();
chance.actions[2].ticker = btc_usd.clone();
}
if chance.profit > OrderedFloat(0.0) {
log::info!("profit: {}", chance.profit.into_inner());
log::info!("full_orderbook: \n{:#?}", (*full_orderbook));
log::info!("chance \n{chance:#?}");
panic!()
}
}
}
fn triangular_chance_sequence(
symbol_btc_usd: &str,
symbol_eth_btc: &str,
symbol_eth_usd: &str,
orderbook_btc_usd: &Orderbook,
orderbook_eth_btc: &Orderbook,
orderbook_eth_usd: &Orderbook,
symbol_map: Arc<Mutex<BTreeMap<String, SymbolInfo>>>,
usd_amount: f64,
) -> TriangularArbitrageChance {
let (btc_usd_bid, btc_usd_bid_volume) = orderbook_btc_usd.bid.last_key_value().unwrap();
let (eth_btc_bid, eth_btc_bid_volume) = orderbook_eth_btc.bid.last_key_value().unwrap();
let (eth_usd_bid, eth_usd_bid_volume) = orderbook_eth_usd.bid.last_key_value().unwrap();
let (btc_usd_ask, btc_usd_ask_volume) = orderbook_btc_usd.ask.first_key_value().unwrap();
let (eth_btc_ask, eth_btc_ask_volume) = orderbook_eth_btc.ask.first_key_value().unwrap();
let (eth_usd_ask, eth_usd_ask_volume) = orderbook_eth_usd.ask.first_key_value().unwrap();
let symbol_map = symbol_map.lock().unwrap();
let btc_usd_info = &symbol_map.get(symbol_btc_usd).unwrap();
let eth_btc_info = &symbol_map.get(symbol_eth_btc).unwrap();
let eth_usd_info = &symbol_map.get(symbol_eth_usd).unwrap();
let trading_fee = 0.001;
triangular_chance_sequence_f64(
PairProfile {
symbol: btc_usd_info.symbol.clone(),
ask: btc_usd_ask.into_inner(),
ask_volume: btc_usd_ask_volume.into_inner(),
bid: btc_usd_bid.into_inner(),
bid_volume: btc_usd_bid_volume.into_inner(),
quote_available: usd_amount,
trading_min: *btc_usd_info.base_min,
trading_increment: *btc_usd_info.base_increment,
trading_fee,
},
PairProfile {
symbol: eth_btc_info.symbol.clone(),
ask: eth_btc_ask.into_inner(),
ask_volume: eth_btc_ask_volume.into_inner(),
bid: eth_btc_bid.into_inner(),
bid_volume: eth_btc_bid_volume.into_inner(),
quote_available: 0.0,
trading_min: *eth_btc_info.base_min,
trading_increment: *eth_btc_info.base_increment,
trading_fee,
},
PairProfile {
symbol: eth_btc_info.symbol.clone(),
ask: eth_usd_ask.into_inner(),
ask_volume: eth_usd_ask_volume.into_inner(),
bid: eth_usd_bid.into_inner(),
bid_volume: eth_usd_bid_volume.into_inner(),
quote_available: 0.0,
trading_min: *eth_usd_info.base_min,
trading_increment: *eth_usd_info.base_increment,
trading_fee,
},
)
}
fn round_amount(amount: f64, increment: f64) -> f64 {
(amount / increment).round() * increment
}
struct PairProfile {
symbol: String,
ask: f64,
ask_volume: f64,
bid: f64,
bid_volume: f64,
quote_available: f64,
trading_min: f64,
trading_increment: f64,
trading_fee: f64,
}
fn triangular_chance_sequence_f64(
btc_usd: PairProfile,
eth_btc: PairProfile,
eth_usd: PairProfile,
) -> TriangularArbitrageChance {
let fee: f64 = 0.001;
let weight: f64 = 1.0 - fee;
let max_usd_bbs: f64 = btc_usd
.ask_volume
.min(eth_btc.ask_volume * btc_usd.bid)
.min(eth_usd.bid_volume * eth_btc.ask);
let max_usd_bss: f64 = eth_usd
.ask_volume
.min(eth_btc.bid_volume * eth_usd.bid)
.min(btc_usd.bid_volume * eth_btc.bid);
let usd_before_bbs = btc_usd.quote_available.min(max_usd_bbs);
let usd_before_bss = btc_usd.quote_available.min(max_usd_bss);
let trade_amounts_bbs_0 = round_amount(usd_before_bbs, btc_usd.trading_increment);
let trade_amounts_bbs_1 = round_amount(
trade_amounts_bbs_0 * weight / btc_usd.ask,
eth_btc.trading_increment,
);
let trade_amounts_bbs_2 = round_amount(
trade_amounts_bbs_1 * weight / eth_btc.bid,
eth_usd.trading_increment,
);
let trade_amounts_bss_0 = round_amount(usd_before_bss, eth_usd.trading_increment);
let trade_amounts_bss_1 = round_amount(
trade_amounts_bss_0 * weight / eth_usd.ask,
eth_btc.trading_increment,
);
let trade_amounts_bss_2 = round_amount(
trade_amounts_bss_1 * weight / eth_btc.bid,
btc_usd.trading_increment,
);
let usd_after_bbs = trade_amounts_bbs_0 / btc_usd.ask * trade_amounts_bbs_1 * (1.0 - fee)
/ eth_btc.ask
* eth_usd.bid
* (1.0 - fee);
let usd_after_bss = trade_amounts_bss_0 / eth_usd.ask * trade_amounts_bss_1 / eth_btc.bid
* btc_usd.bid
* pow(weight, 3);
let profit_bbs = usd_after_bbs - usd_before_bbs;
let profit_bss = usd_after_bss - usd_before_bss;
log::info!(
"BBS profit ({},{},{}): {profit_bbs}",
btc_usd.symbol,
eth_btc.symbol,
eth_usd.symbol
);
log::info!(
"BSS profit ({},{},{}): {profit_bss}",
eth_usd.symbol,
eth_btc.symbol,
btc_usd.symbol
);
if profit_bbs >= profit_bss {
return TriangularArbitrageChance {
profit: OrderedFloat(profit_bbs),
actions: [
ActionInfo::buy(OrderedFloat(btc_usd.ask), OrderedFloat(trade_amounts_bbs_0)),
ActionInfo::buy(OrderedFloat(eth_btc.ask), OrderedFloat(trade_amounts_bbs_1)),
ActionInfo::sell(OrderedFloat(eth_usd.bid), OrderedFloat(trade_amounts_bbs_2)),
],
};
}
TriangularArbitrageChance {
profit: OrderedFloat(profit_bss),
actions: [
ActionInfo::buy(OrderedFloat(eth_usd.ask), OrderedFloat(trade_amounts_bss_0)),
ActionInfo::sell(OrderedFloat(eth_btc.bid), OrderedFloat(trade_amounts_bss_1)),
ActionInfo::sell(OrderedFloat(btc_usd.bid), OrderedFloat(trade_amounts_bss_2)),
],
}
}