Custom HummingBot for Whitebit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
hummingbot/scripts/simple_xemm_example.py

204 lines
12 KiB

from decimal import Decimal
import pandas as pd
from hummingbot.core.data_type.common import OrderType, TradeType
from hummingbot.core.data_type.order_candidate import OrderCandidate
from hummingbot.core.event.events import OrderFilledEvent
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
class SimpleXEMM(ScriptStrategyBase):
"""
BotCamp Cohort: Sept 2022
Design Template: https://hummingbot-foundation.notion.site/Simple-XEMM-Example-f08cf7546ea94a44b389672fd21bb9ad
Video: https://www.loom.com/share/ca08fe7bc3d14ba68ae704305ac78a3a
Description:
A simplified version of Hummingbot cross-exchange market making strategy, this bot makes a market on
the maker pair and hedges any filled trades in the taker pair. If the spread (difference between maker order price
and taker hedge price) dips below min_spread, the bot refreshes the order
"""
maker_exchange = "kucoin_paper_trade"
maker_pair = "ETH-USDT"
taker_exchange = "gate_io_paper_trade"
taker_pair = "ETH-USDT"
order_amount = 0.1 # amount for each order
spread_bps = 10 # bot places maker orders at this spread to taker price
min_spread_bps = 0 # bot refreshes order if spread is lower than min-spread
slippage_buffer_spread_bps = 100 # buffer applied to limit taker hedging trades on taker exchange
max_order_age = 120 # bot refreshes orders after this age
markets = {maker_exchange: {maker_pair}, taker_exchange: {taker_pair}}
buy_order_placed = False
sell_order_placed = False
def on_tick(self):
taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
if not self.buy_order_placed:
maker_buy_price = taker_sell_result.result_price * Decimal(1 - self.spread_bps / 10000)
buy_order_amount = min(self.order_amount, self.buy_hedging_budget())
buy_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(buy_order_amount), price=maker_buy_price)
buy_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False)
self.buy(self.maker_exchange, self.maker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price)
self.buy_order_placed = True
if not self.sell_order_placed:
maker_sell_price = taker_buy_result.result_price * Decimal(1 + self.spread_bps / 10000)
sell_order_amount = min(self.order_amount, self.sell_hedging_budget())
sell_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(sell_order_amount), price=maker_sell_price)
sell_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False)
self.sell(self.maker_exchange, self.maker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price)
self.sell_order_placed = True
for order in self.get_active_orders(connector_name=self.maker_exchange):
cancel_timestamp = order.creation_timestamp / 1000000 + self.max_order_age
if order.is_buy:
buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000)
if order.price > buy_cancel_threshold or cancel_timestamp < self.current_timestamp:
self.logger().info(f"Cancelling buy order: {order.client_order_id}")
self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id)
self.buy_order_placed = False
else:
sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000)
if order.price < sell_cancel_threshold or cancel_timestamp < self.current_timestamp:
self.logger().info(f"Cancelling sell order: {order.client_order_id}")
self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id)
self.sell_order_placed = False
return
def buy_hedging_budget(self) -> Decimal:
balance = self.connectors[self.taker_exchange].get_available_balance("ETH")
return balance
def sell_hedging_budget(self) -> Decimal:
balance = self.connectors[self.taker_exchange].get_available_balance("USDT")
taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
return balance / taker_buy_result.result_price
def is_active_maker_order(self, event: OrderFilledEvent):
"""
Helper function that checks if order is an active order on the maker exchange
"""
for order in self.get_active_orders(connector_name=self.maker_exchange):
if order.client_order_id == event.order_id:
return True
return False
def did_fill_order(self, event: OrderFilledEvent):
mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
if event.trade_type == TradeType.BUY and self.is_active_maker_order(event):
taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
sell_price_with_slippage = taker_sell_result.result_price * Decimal(1 - self.slippage_buffer_spread_bps / 10000)
self.logger().info(f"Filled maker buy order with price: {event.price}")
sell_spread_bps = (taker_sell_result.result_price - event.price) / mid_price * 10000
self.logger().info(f"Sending taker sell order at price: {taker_sell_result.result_price} spread: {int(sell_spread_bps)} bps")
sell_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(event.amount), price=sell_price_with_slippage)
sell_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False)
self.sell(self.taker_exchange, self.taker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price)
self.buy_order_placed = False
else:
if event.trade_type == TradeType.SELL and self.is_active_maker_order(event):
taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
buy_price_with_slippage = taker_buy_result.result_price * Decimal(1 + self.slippage_buffer_spread_bps / 10000)
buy_spread_bps = (event.price - taker_buy_result.result_price) / mid_price * 10000
self.logger().info(f"Filled maker sell order at price: {event.price}")
self.logger().info(f"Sending taker buy order: {taker_buy_result.result_price} spread: {int(buy_spread_bps)}")
buy_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(event.amount), price=buy_price_with_slippage)
buy_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False)
self.buy(self.taker_exchange, self.taker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price)
self.sell_order_placed = False
def exchanges_df(self) -> pd.DataFrame:
"""
Return a custom data frame of prices on maker vs taker exchanges for display purposes
"""
mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
maker_buy_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
maker_sell_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
maker_buy_spread_bps = (maker_buy_result.result_price - taker_buy_result.result_price) / mid_price * 10000
maker_sell_spread_bps = (taker_sell_result.result_price - maker_sell_result.result_price) / mid_price * 10000
columns = ["Exchange", "Market", "Mid Price", "Buy Price", "Sell Price", "Buy Spread", "Sell Spread"]
data = []
data.append([
self.maker_exchange,
self.maker_pair,
float(self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)),
float(maker_buy_result.result_price),
float(maker_sell_result.result_price),
int(maker_buy_spread_bps),
int(maker_sell_spread_bps)
])
data.append([
self.taker_exchange,
self.taker_pair,
float(self.connectors[self.taker_exchange].get_mid_price(self.maker_pair)),
float(taker_buy_result.result_price),
float(taker_sell_result.result_price),
int(-maker_buy_spread_bps),
int(-maker_sell_spread_bps)
])
df = pd.DataFrame(data=data, columns=columns)
return df
def active_orders_df(self) -> pd.DataFrame:
"""
Returns a custom data frame of all active maker orders for display purposes
"""
columns = ["Exchange", "Market", "Side", "Price", "Amount", "Spread Mid", "Spread Cancel", "Age"]
data = []
mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)
taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount)
taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount)
buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000)
sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000)
for connector_name, connector in self.connectors.items():
for order in self.get_active_orders(connector_name):
age_txt = "n/a" if order.age() <= 0. else pd.Timestamp(order.age(), unit='s').strftime('%H:%M:%S')
spread_mid_bps = (mid_price - order.price) / mid_price * 10000 if order.is_buy else (order.price - mid_price) / mid_price * 10000
spread_cancel_bps = (buy_cancel_threshold - order.price) / buy_cancel_threshold * 10000 if order.is_buy else (order.price - sell_cancel_threshold) / sell_cancel_threshold * 10000
data.append([
self.maker_exchange,
order.trading_pair,
"buy" if order.is_buy else "sell",
float(order.price),
float(order.quantity),
int(spread_mid_bps),
int(spread_cancel_bps),
age_txt
])
if not data:
raise ValueError
df = pd.DataFrame(data=data, columns=columns)
df.sort_values(by=["Market", "Side"], inplace=True)
return df
def format_status(self) -> str:
"""
Returns status of the current strategy on user balances and current active orders. This function is called
when status command is issued. Override this function to create custom status display output.
"""
if not self.ready_to_trade:
return "Market connectors are not ready."
lines = []
balance_df = self.get_balance_df()
lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")])
exchanges_df = self.exchanges_df()
lines.extend(["", " Exchanges:"] + [" " + line for line in exchanges_df.to_string(index=False).split("\n")])
try:
orders_df = self.active_orders_df()
lines.extend(["", " Active Orders:"] + [" " + line for line in orders_df.to_string(index=False).split("\n")])
except ValueError:
lines.extend(["", " No active maker orders."])
return "\n".join(lines)