diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 30e803ce39..1c72502953 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -81,7 +81,7 @@ def list_configs(self, # type: HummingbotApplication self._notify("\n".join(lines)) if self.strategy_name is not None: - data = [[cv.key, cv.value] for cv in self.strategy_config_map.values() if not cv.is_secure] + data = [[cv.printable_key or cv.key, cv.value] for cv in self.strategy_config_map.values() if not cv.is_secure] df = pd.DataFrame(data=data, columns=columns) self._notify("\nStrategy Configurations:") lines = [" " + line for line in df.to_string(index=False, max_colwidth=50).split("\n")] diff --git a/hummingbot/client/config/config_var.py b/hummingbot/client/config/config_var.py index 85811bf2fb..6a346653b9 100644 --- a/hummingbot/client/config/config_var.py +++ b/hummingbot/client/config/config_var.py @@ -24,7 +24,8 @@ def __init__(self, # Whether to prompt a user for value when new strategy config file is created prompt_on_new: bool = False, # Whether this is a config var used in connect command - is_connect_key: bool = False): + is_connect_key: bool = False, + printable_key: str = None): self.prompt = prompt self.key = key self.value = None @@ -36,6 +37,7 @@ def __init__(self, self._on_validated = on_validated self.prompt_on_new = prompt_on_new self.is_connect_key = is_connect_key + self.printable_key = printable_key async def get_prompt(self): if inspect.iscoroutinefunction(self.prompt): diff --git a/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_order_book_tracker.py b/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_order_book_tracker.py index e4df92f7b9..c25ba5444b 100644 --- a/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_order_book_tracker.py +++ b/hummingbot/connector/derivative/binance_perpetual/binance_perpetual_order_book_tracker.py @@ -80,10 +80,8 @@ async def _order_book_diff_router(self): now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages process: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -128,14 +126,13 @@ async def _track_single_book(self, trading_pair: str): now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: past_diffs: List[OrderBookMessage] = list(past_diffs_window) order_book.restore_from_snapshot_and_diffs(message, past_diffs) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py index 0801dd47be..e982f5c85b 100644 --- a/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py +++ b/hummingbot/connector/exchange/ascend_ex/ascend_ex_order_book_tracker.py @@ -85,8 +85,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -100,7 +99,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/bamboo_relay/bamboo_relay_order_book_tracker.py b/hummingbot/connector/exchange/bamboo_relay/bamboo_relay_order_book_tracker.py index ef08e8c989..f29546a2b2 100644 --- a/hummingbot/connector/exchange/bamboo_relay/bamboo_relay_order_book_tracker.py +++ b/hummingbot/connector/exchange/bamboo_relay/bamboo_relay_order_book_tracker.py @@ -130,10 +130,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -175,7 +173,7 @@ async def _track_single_book(self, trading_pair: str): s_bids, s_asks = active_order_tracker.convert_snapshot_message_to_order_book_row(message) order_book.apply_snapshot(s_bids, s_asks, message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py index d730dc97c7..3ba8c17580 100644 --- a/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py +++ b/hummingbot/connector/exchange/beaxy/beaxy_order_book_tracker.py @@ -82,10 +82,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug('Messages processed: %d, rejected: %d, queued: %d', - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -163,8 +161,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug('Processed %d order book diffs for %s.', - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -178,7 +175,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug('Processed order book snapshot for %s.', trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/binance/binance_exchange.pyx b/hummingbot/connector/exchange/binance/binance_exchange.pyx index ec048f8475..8601b0731d 100755 --- a/hummingbot/connector/exchange/binance/binance_exchange.pyx +++ b/hummingbot/connector/exchange/binance/binance_exchange.pyx @@ -402,7 +402,7 @@ cdef class BinanceExchange(ExchangeBase): trading_pairs = list(trading_pairs_to_order_map.keys()) tasks = [self.query_api(self._binance_client.get_my_trades, symbol=convert_to_exchange_trading_pair(trading_pair)) for trading_pair in trading_pairs] - self.logger().debug("Polling for order fills of %d trading pairs.", len(tasks)) + self.logger().debug(f"Polling for order fills of {len(tasks)} trading pairs.") results = await safe_gather(*tasks, return_exceptions=True) for trades, trading_pair in zip(results, trading_pairs): order_map = trading_pairs_to_order_map[trading_pair] @@ -448,7 +448,7 @@ cdef class BinanceExchange(ExchangeBase): trading_pairs = self._order_book_tracker._trading_pairs tasks = [self.query_api(self._binance_client.get_my_trades, symbol=convert_to_exchange_trading_pair(trading_pair)) for trading_pair in trading_pairs] - self.logger().debug("Polling for order fills of %d trading pairs.", len(tasks)) + self.logger().debug(f"Polling for order fills of {len(tasks)} trading pairs.") exchange_history = await safe_gather(*tasks, return_exceptions=True) for trades, trading_pair in zip(exchange_history, trading_pairs): if isinstance(trades, Exception): @@ -494,7 +494,7 @@ cdef class BinanceExchange(ExchangeBase): tasks = [self.query_api(self._binance_client.get_order, symbol=convert_to_exchange_trading_pair(o.trading_pair), origClientOrderId=o.client_order_id) for o in tracked_orders] - self.logger().debug("Polling for order status updates of %d orders.", len(tasks)) + self.logger().debug(f"Polling for order status updates of {len(tasks)} orders.") results = await safe_gather(*tasks, return_exceptions=True) for order_update, tracked_order in zip(results, tracked_orders): client_order_id = tracked_order.client_order_id diff --git a/hummingbot/connector/exchange/binance/binance_order_book_tracker.py b/hummingbot/connector/exchange/binance/binance_order_book_tracker.py index 40edaad388..55481ae47a 100644 --- a/hummingbot/connector/exchange/binance/binance_order_book_tracker.py +++ b/hummingbot/connector/exchange/binance/binance_order_book_tracker.py @@ -80,10 +80,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -129,14 +127,13 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: past_diffs: List[OrderBookMessage] = list(past_diffs_window) order_book.restore_from_snapshot_and_diffs(message, past_diffs) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/bitfinex/bitfinex_order_book_tracker.py b/hummingbot/connector/exchange/bitfinex/bitfinex_order_book_tracker.py index 9efdbb568a..68be932262 100644 --- a/hummingbot/connector/exchange/bitfinex/bitfinex_order_book_tracker.py +++ b/hummingbot/connector/exchange/bitfinex/bitfinex_order_book_tracker.py @@ -118,10 +118,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / CALC_STAT_MINUTE) > int(last_message_timestamp / CALC_STAT_MINUTE): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -171,8 +169,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / CALC_STAT_MINUTE) > int(last_message_timestamp / CALC_STAT_MINUTE): - self.logger().debug( - "Processed %d order book diffs for %s.", diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now @@ -193,7 +190,7 @@ async def _track_single_book(self, trading_pair: str): ) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception as err: diff --git a/hummingbot/connector/exchange/bittrex/bittrex_order_book_tracker.py b/hummingbot/connector/exchange/bittrex/bittrex_order_book_tracker.py index 4b779eba98..1a103368d3 100644 --- a/hummingbot/connector/exchange/bittrex/bittrex_order_book_tracker.py +++ b/hummingbot/connector/exchange/bittrex/bittrex_order_book_tracker.py @@ -184,7 +184,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/blocktane/blocktane_order_book_tracker.py b/hummingbot/connector/exchange/blocktane/blocktane_order_book_tracker.py index beb3d3d66d..feec224339 100644 --- a/hummingbot/connector/exchange/blocktane/blocktane_order_book_tracker.py +++ b/hummingbot/connector/exchange/blocktane/blocktane_order_book_tracker.py @@ -77,10 +77,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - # self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - # messages_accepted, - # messages_rejected, - # messages_queued) + # self.logger().debug(f"Diff messages processed: {messages_accepted}, " + # f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -127,14 +125,13 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - # self.logger().debug("Processed %d order book diffs for %s.", - # diff_messages_accepted, trading_pair) + # self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: past_diffs: List[OrderBookMessage] = list(past_diffs_window) order_book.restore_from_snapshot_and_diffs(message, past_diffs) - # self.logger().debug("Processed order book snapshot for %s.", trading_pair) + # self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/coinbase_pro/coinbase_pro_order_book_tracker.py b/hummingbot/connector/exchange/coinbase_pro/coinbase_pro_order_book_tracker.py index ea8f5f4b6a..ebacd90075 100644 --- a/hummingbot/connector/exchange/coinbase_pro/coinbase_pro_order_book_tracker.py +++ b/hummingbot/connector/exchange/coinbase_pro/coinbase_pro_order_book_tracker.py @@ -99,10 +99,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -153,8 +151,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -168,7 +165,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/crypto_com/crypto_com_order_book_tracker.py b/hummingbot/connector/exchange/crypto_com/crypto_com_order_book_tracker.py index 8cf9223b46..cc25e14fc7 100644 --- a/hummingbot/connector/exchange/crypto_com/crypto_com_order_book_tracker.py +++ b/hummingbot/connector/exchange/crypto_com/crypto_com_order_book_tracker.py @@ -83,8 +83,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -98,7 +97,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/dolomite/dolomite_order_book_tracker.py b/hummingbot/connector/exchange/dolomite/dolomite_order_book_tracker.py index 5c2e50b388..d460d67804 100644 --- a/hummingbot/connector/exchange/dolomite/dolomite_order_book_tracker.py +++ b/hummingbot/connector/exchange/dolomite/dolomite_order_book_tracker.py @@ -107,7 +107,7 @@ async def _track_single_book(self, trading_pair: str): elif message.type is OrderBookMessageType.SNAPSHOT: s_bids, s_asks = active_order_tracker.convert_snapshot_message_to_order_book_row(message) order_book.apply_snapshot(s_bids, s_asks, message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise diff --git a/hummingbot/connector/exchange/dydx/dydx_order_book_tracker.py b/hummingbot/connector/exchange/dydx/dydx_order_book_tracker.py index 1450c57156..67763d305e 100644 --- a/hummingbot/connector/exchange/dydx/dydx_order_book_tracker.py +++ b/hummingbot/connector/exchange/dydx/dydx_order_book_tracker.py @@ -86,7 +86,7 @@ async def _track_single_book(self, trading_pair: str): elif message.type is OrderBookMessageType.SNAPSHOT: s_bids, s_asks = active_order_tracker.convert_snapshot_message_to_order_book_row(message) order_book.apply_snapshot(s_bids, s_asks, int(message.timestamp)) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise diff --git a/hummingbot/connector/exchange/huobi/huobi_order_book_tracker.py b/hummingbot/connector/exchange/huobi/huobi_order_book_tracker.py index 39c83f429b..f66d0e0053 100644 --- a/hummingbot/connector/exchange/huobi/huobi_order_book_tracker.py +++ b/hummingbot/connector/exchange/huobi/huobi_order_book_tracker.py @@ -78,10 +78,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -114,13 +112,12 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: order_book.apply_snapshot(message.bids, message.asks, message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py b/hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py index 8ac727f7d5..cce7530271 100644 --- a/hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py +++ b/hummingbot/connector/exchange/kraken/kraken_order_book_tracker.py @@ -78,9 +78,7 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d", - messages_accepted, - messages_rejected) + self.logger().debug(f"Diff messages processed: {messages_accepted}, rejected: {messages_rejected}") messages_accepted = 0 messages_rejected = 0 diff --git a/hummingbot/connector/exchange/kucoin/kucoin_order_book_tracker.py b/hummingbot/connector/exchange/kucoin/kucoin_order_book_tracker.py index 8b42c7db4f..10764f2b08 100644 --- a/hummingbot/connector/exchange/kucoin/kucoin_order_book_tracker.py +++ b/hummingbot/connector/exchange/kucoin/kucoin_order_book_tracker.py @@ -75,10 +75,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -127,8 +125,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -142,7 +139,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/liquid/liquid_order_book_tracker.py b/hummingbot/connector/exchange/liquid/liquid_order_book_tracker.py index 75b2d50f68..d9c11e0278 100644 --- a/hummingbot/connector/exchange/liquid/liquid_order_book_tracker.py +++ b/hummingbot/connector/exchange/liquid/liquid_order_book_tracker.py @@ -80,10 +80,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -146,15 +144,14 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: past_diffs: List[OrderBookMessage] = list(past_diffs_window) past_diffs_window.append(message) order_book.restore_from_snapshot_and_diffs(message, past_diffs) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/loopring/loopring_order_book_tracker.py b/hummingbot/connector/exchange/loopring/loopring_order_book_tracker.py index 1f43d627d4..0a9decf1da 100644 --- a/hummingbot/connector/exchange/loopring/loopring_order_book_tracker.py +++ b/hummingbot/connector/exchange/loopring/loopring_order_book_tracker.py @@ -90,7 +90,7 @@ async def _track_single_book(self, trading_pair: str): elif message.type is OrderBookMessageType.SNAPSHOT: s_bids, s_asks = active_order_tracker.convert_snapshot_message_to_order_book_row(message) order_book.apply_snapshot(s_bids, s_asks, message.timestamp) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise diff --git a/hummingbot/connector/exchange/okex/okex_order_book_tracker.py b/hummingbot/connector/exchange/okex/okex_order_book_tracker.py index e29a82c743..a09a5731fe 100644 --- a/hummingbot/connector/exchange/okex/okex_order_book_tracker.py +++ b/hummingbot/connector/exchange/okex/okex_order_book_tracker.py @@ -69,10 +69,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -107,13 +105,12 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: order_book.apply_snapshot(message.bids, message.asks, message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/probit/probit_order_book_tracker.py b/hummingbot/connector/exchange/probit/probit_order_book_tracker.py index e7ac692ba1..cb5f4f9ab4 100644 --- a/hummingbot/connector/exchange/probit/probit_order_book_tracker.py +++ b/hummingbot/connector/exchange/probit/probit_order_book_tracker.py @@ -85,8 +85,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -100,7 +99,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = probit_utils.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/connector/exchange/radar_relay/radar_relay_order_book_tracker.py b/hummingbot/connector/exchange/radar_relay/radar_relay_order_book_tracker.py index 65c6da593f..34c35917e2 100644 --- a/hummingbot/connector/exchange/radar_relay/radar_relay_order_book_tracker.py +++ b/hummingbot/connector/exchange/radar_relay/radar_relay_order_book_tracker.py @@ -128,10 +128,8 @@ async def _order_book_diff_router(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Diff messages processed: %d, rejected: %d, queued: %d", - messages_accepted, - messages_rejected, - messages_queued) + self.logger().debug(f"Diff messages processed: {messages_accepted}, " + f"rejected: {messages_rejected}, queued: {messages_queued}") messages_accepted = 0 messages_rejected = 0 messages_queued = 0 @@ -179,8 +177,7 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: @@ -194,7 +191,7 @@ async def _track_single_book(self, trading_pair: str): d_bids, d_asks = active_order_tracker.convert_diff_message_to_order_book_row(diff_message) order_book.apply_diffs(d_bids, d_asks, diff_message.update_id) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: diff --git a/hummingbot/core/data_type/order_book_tracker.py b/hummingbot/core/data_type/order_book_tracker.py index 36ca95179c..b5a92b2d24 100644 --- a/hummingbot/core/data_type/order_book_tracker.py +++ b/hummingbot/core/data_type/order_book_tracker.py @@ -264,14 +264,13 @@ async def _track_single_book(self, trading_pair: str): # Output some statistics periodically. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Processed %d order book diffs for %s.", - diff_messages_accepted, trading_pair) + self.logger().debug(f"Processed {diff_messages_accepted} order book diffs for {trading_pair}.") diff_messages_accepted = 0 last_message_timestamp = now elif message.type is OrderBookMessageType.SNAPSHOT: past_diffs: List[OrderBookMessage] = list(past_diffs_window) order_book.restore_from_snapshot_and_diffs(message, past_diffs) - self.logger().debug("Processed order book snapshot for %s.", trading_pair) + self.logger().debug(f"Processed order book snapshot for {trading_pair}.") except asyncio.CancelledError: raise except Exception: @@ -307,9 +306,7 @@ async def _emit_trade_event_loop(self): # Log some statistics. now: float = time.time() if int(now / 60.0) > int(last_message_timestamp / 60.0): - self.logger().debug("Trade messages processed: %d, rejected: %d", - messages_accepted, - messages_rejected) + self.logger().debug(f"Trade messages processed: {messages_accepted}, rejected: {messages_rejected}") messages_accepted = 0 messages_rejected = 0 diff --git a/hummingbot/strategy/__utils__/trailing_indicators/average_volatility.py b/hummingbot/strategy/__utils__/trailing_indicators/average_volatility.py new file mode 100644 index 0000000000..cf57615e80 --- /dev/null +++ b/hummingbot/strategy/__utils__/trailing_indicators/average_volatility.py @@ -0,0 +1,14 @@ +from .base_trailing_indicator import BaseTrailingIndicator +import numpy as np + + +class AverageVolatilityIndicator(BaseTrailingIndicator): + def __init__(self, sampling_length: int = 30, processing_length: int = 15): + super().__init__(sampling_length, processing_length) + + def _indicator_calculation(self) -> float: + return np.var(self._sampling_buffer.get_as_numpy_array()) + + def _processing_calculation(self) -> float: + processing_array = self._processing_buffer.get_as_numpy_array() + return np.sqrt(np.mean(processing_array)) diff --git a/hummingbot/strategy/__utils__/trailing_indicators/base_trailing_indicator.py b/hummingbot/strategy/__utils__/trailing_indicators/base_trailing_indicator.py new file mode 100644 index 0000000000..8df46b58b0 --- /dev/null +++ b/hummingbot/strategy/__utils__/trailing_indicators/base_trailing_indicator.py @@ -0,0 +1,49 @@ +from abc import ABC, abstractmethod +import numpy as np +import logging +from ..ring_buffer import RingBuffer + +pmm_logger = None + + +class BaseTrailingIndicator(ABC): + @classmethod + def logger(cls): + global pmm_logger + if pmm_logger is None: + pmm_logger = logging.getLogger(__name__) + return pmm_logger + + def __init__(self, sampling_length: int = 30, processing_length: int = 15): + self._sampling_length = sampling_length + self._sampling_buffer = RingBuffer(sampling_length) + self._processing_length = processing_length + self._processing_buffer = RingBuffer(processing_length) + + def add_sample(self, value: float): + self._sampling_buffer.add_value(value) + indicator_value = self._indicator_calculation() + self._processing_buffer.add_value(indicator_value) + + @abstractmethod + def _indicator_calculation(self) -> float: + raise NotImplementedError + + def _processing_calculation(self) -> float: + """ + Processing of the processing buffer to return final value. + Default behavior is buffer average + """ + return np.mean(self._processing_buffer.get_as_numpy_array()) + + @property + def current_value(self) -> float: + return self._processing_calculation() + + @property + def is_sampling_buffer_full(self) -> bool: + return self._sampling_buffer.is_full + + @property + def is_processing_buffer_full(self) -> bool: + return self._processing_buffer.is_full diff --git a/hummingbot/strategy/__utils__/trailing_indicators/exponential_moving_average.py b/hummingbot/strategy/__utils__/trailing_indicators/exponential_moving_average.py new file mode 100644 index 0000000000..ed380fc99a --- /dev/null +++ b/hummingbot/strategy/__utils__/trailing_indicators/exponential_moving_average.py @@ -0,0 +1,17 @@ +from base_trailing_indicator import BaseTrailingIndicator +import pandas as pd + + +class ExponentialMovingAverageIndicator(BaseTrailingIndicator): + def __init__(self, sampling_length: int = 30, processing_length: int = 1): + if processing_length != 1: + raise Exception("Exponential moving average processing_length should be 1") + super().__init__(sampling_length, processing_length) + + def _indicator_calculation(self) -> float: + ema = pd.Series(self._sampling_buffer.get_as_numpy_array())\ + .ewm(span=self._sampling_length, adjust=True).mean() + return ema[-1] + + def _processing_calculation(self) -> float: + return self._processing_buffer.get_last_value() diff --git a/hummingbot/strategy/avellaneda_market_making/__init__.py b/hummingbot/strategy/avellaneda_market_making/__init__.py new file mode 100644 index 0000000000..d29aaf1e02 --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +from .avellaneda_market_making import AvellanedaMarketMakingStrategy +__all__ = [ + AvellanedaMarketMakingStrategy, +] diff --git a/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd new file mode 100644 index 0000000000..79df1f715f --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd @@ -0,0 +1,73 @@ +# distutils: language=c++ + +from libc.stdint cimport int64_t +from hummingbot.strategy.strategy_base cimport StrategyBase + + +cdef class AvellanedaMarketMakingStrategy(StrategyBase): + cdef: + object _market_info + object _minimum_spread + object _order_amount + double _order_refresh_time + double _max_order_age + object _order_refresh_tolerance_pct + double _filled_order_delay + object _inventory_target_base_pct + bint _order_optimization_enabled + bint _add_transaction_costs_to_orders + bint _hb_app_notification + bint _is_debug + + double _cancel_timestamp + double _create_timestamp + object _limit_order_type + bint _all_markets_ready + int _filled_buys_balance + int _filled_sells_balance + double _last_timestamp + double _status_report_interval + int64_t _logging_options + object _last_own_trade_price + int _volatility_sampling_period + double _last_sampling_timestamp + bint _parameters_based_on_spread + int _ticks_to_be_ready + object _min_spread + object _max_spread + object _vol_to_spread_multiplier + object _inventory_risk_aversion + object _kappa + object _gamma + object _eta + object _closing_time + object _time_left + object _q_adjustment_factor + object _reserved_price + object _optimal_spread + object _optimal_bid + object _optimal_ask + object _latest_parameter_calculation_vol + str _debug_csv_path + object _avg_vol + + cdef object c_get_mid_price(self) + cdef object c_create_base_proposal(self) + cdef tuple c_get_adjusted_available_balance(self, list orders) + cdef c_apply_order_price_modifiers(self, object proposal) + cdef c_apply_order_amount_eta_transformation(self, object proposal) + cdef c_apply_budget_constraint(self, object proposal) + cdef c_apply_order_optimization(self, object proposal) + cdef c_apply_add_transaction_costs(self, object proposal) + cdef bint c_is_within_tolerance(self, list current_prices, list proposal_prices) + cdef c_cancel_active_orders(self, object proposal) + cdef c_aged_order_refresh(self) + cdef bint c_to_create_orders(self, object proposal) + cdef c_execute_orders_proposal(self, object proposal) + cdef set_timers(self) + cdef double c_get_spread(self) + cdef c_collect_market_variables(self, double timestamp) + cdef bint c_is_algorithm_ready(self) + cdef c_calculate_reserved_price_and_optimal_spread(self) + cdef object c_calculate_target_inventory(self) + cdef c_recalculate_parameters(self) diff --git a/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx new file mode 100644 index 0000000000..9704ec2032 --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx @@ -0,0 +1,1022 @@ +from decimal import Decimal +import logging +import pandas as pd +import numpy as np +from typing import ( + List, + Dict, +) +from math import ( + floor, + ceil +) +import time +import datetime +import os +from hummingbot.core.clock cimport Clock +from hummingbot.core.event.events import TradeType +from hummingbot.core.data_type.limit_order cimport LimitOrder +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.network_iterator import NetworkStatus +from hummingbot.connector.exchange_base import ExchangeBase +from hummingbot.connector.exchange_base cimport ExchangeBase +from hummingbot.core.event.events import OrderType + +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.strategy_base import StrategyBase +from hummingbot.client.config.global_config_map import global_config_map + +from .data_types import ( + Proposal, + PriceSize +) +from ..order_tracker cimport OrderTracker +from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator + + +NaN = float("nan") +s_decimal_zero = Decimal(0) +s_decimal_neg_one = Decimal(-1) +s_decimal_one = Decimal(1) +pmm_logger = None + + +cdef class AvellanedaMarketMakingStrategy(StrategyBase): + OPTION_LOG_CREATE_ORDER = 1 << 3 + OPTION_LOG_MAKER_ORDER_FILLED = 1 << 4 + OPTION_LOG_STATUS_REPORT = 1 << 5 + OPTION_LOG_ALL = 0x7fffffffffffffff + + # These are exchanges where you're expected to expire orders instead of actively cancelling them. + RADAR_RELAY_TYPE_EXCHANGES = {"radar_relay", "bamboo_relay"} + + @classmethod + def logger(cls): + global pmm_logger + if pmm_logger is None: + pmm_logger = logging.getLogger(__name__) + return pmm_logger + + def __init__(self, + market_info: MarketTradingPairTuple, + order_amount: Decimal, + order_refresh_time: float = 30.0, + max_order_age = 1800.0, + order_refresh_tolerance_pct: Decimal = s_decimal_neg_one, + order_optimization_enabled = True, + filled_order_delay: float = 60.0, + inventory_target_base_pct: Decimal = s_decimal_zero, + add_transaction_costs_to_orders: bool = True, + logging_options: int = OPTION_LOG_ALL, + status_report_interval: float = 900, + hb_app_notification: bool = False, + parameters_based_on_spread: bool = True, + min_spread: Decimal = Decimal("0.15"), + max_spread: Decimal = Decimal("2"), + vol_to_spread_multiplier: Decimal = Decimal("1.3"), + inventory_risk_aversion: Decimal = Decimal("0.5"), + order_book_depth_factor: Decimal = Decimal("0.1"), + risk_factor: Decimal = Decimal("0.5"), + order_amount_shape_factor: Decimal = Decimal("0.005"), + closing_time: Decimal = Decimal("1"), + debug_csv_path: str = '', + volatility_buffer_size: int = 30, + is_debug: bool = True, + ): + super().__init__() + self._sb_order_tracker = OrderTracker() + self._market_info = market_info + self._order_amount = order_amount + self._order_optimization_enabled = order_optimization_enabled + self._order_refresh_time = order_refresh_time + self._max_order_age = max_order_age + self._order_refresh_tolerance_pct = order_refresh_tolerance_pct + self._filled_order_delay = filled_order_delay + self._inventory_target_base_pct = inventory_target_base_pct + self._add_transaction_costs_to_orders = add_transaction_costs_to_orders + self._hb_app_notification = hb_app_notification + + self._cancel_timestamp = 0 + self._create_timestamp = 0 + self._limit_order_type = self._market_info.market.get_maker_order_type() + self._all_markets_ready = False + self._filled_buys_balance = 0 + self._filled_sells_balance = 0 + self._logging_options = logging_options + self._last_timestamp = 0 + self._status_report_interval = status_report_interval + self._last_own_trade_price = Decimal('nan') + + self.c_add_markets([market_info.market]) + self._ticks_to_be_ready = volatility_buffer_size + self._parameters_based_on_spread = parameters_based_on_spread + self._min_spread = min_spread + self._max_spread = max_spread + self._vol_to_spread_multiplier = vol_to_spread_multiplier + self._inventory_risk_aversion = inventory_risk_aversion + self._avg_vol = AverageVolatilityIndicator(volatility_buffer_size, 1) + self._last_sampling_timestamp = 0 + self._kappa = order_book_depth_factor + self._gamma = risk_factor + self._eta = order_amount_shape_factor + self._time_left = closing_time + self._closing_time = closing_time + self._latest_parameter_calculation_vol = s_decimal_zero + self._reserved_price = s_decimal_zero + self._optimal_spread = s_decimal_zero + self._optimal_ask = s_decimal_zero + self._optimal_bid = s_decimal_zero + self._debug_csv_path = debug_csv_path + self._is_debug = is_debug + try: + if self._is_debug: + os.unlink(self._debug_csv_path) + except FileNotFoundError: + pass + + def all_markets_ready(self): + return all([market.ready for market in self._sb_markets]) + + @property + def market_info(self) -> MarketTradingPairTuple: + return self._market_info + + @property + def order_refresh_tolerance_pct(self) -> Decimal: + return self._order_refresh_tolerance_pct + + @order_refresh_tolerance_pct.setter + def order_refresh_tolerance_pct(self, value: Decimal): + self._order_refresh_tolerance_pct = value + + @property + def order_amount(self) -> Decimal: + return self._order_amount + + @order_amount.setter + def order_amount(self, value: Decimal): + self._order_amount = value + + @property + def inventory_target_base_pct(self) -> Decimal: + return self._inventory_target_base_pct + + @inventory_target_base_pct.setter + def inventory_target_base_pct(self, value: Decimal): + self._inventory_target_base_pct = value + + @property + def order_optimization_enabled(self) -> bool: + return self._order_optimization_enabled + + @order_optimization_enabled.setter + def order_optimization_enabled(self, value: bool): + self._order_optimization_enabled = value + + @property + def order_refresh_time(self) -> float: + return self._order_refresh_time + + @order_refresh_time.setter + def order_refresh_time(self, value: float): + self._order_refresh_time = value + + @property + def filled_order_delay(self) -> float: + return self._filled_order_delay + + @filled_order_delay.setter + def filled_order_delay(self, value: float): + self._filled_order_delay = value + + @property + def filled_order_delay(self) -> float: + return self._filled_order_delay + + @filled_order_delay.setter + def filled_order_delay(self, value: float): + self._filled_order_delay = value + + @property + def add_transaction_costs_to_orders(self) -> bool: + return self._add_transaction_costs_to_orders + + @add_transaction_costs_to_orders.setter + def add_transaction_costs_to_orders(self, value: bool): + self._add_transaction_costs_to_orders = value + + @property + def base_asset(self): + return self._market_info.base_asset + + @property + def quote_asset(self): + return self._market_info.quote_asset + + @property + def trading_pair(self): + return self._market_info.trading_pair + + def get_price(self) -> float: + return self.get_mid_price() + + def get_last_price(self) -> float: + return self._market_info.get_last_price() + + def get_mid_price(self) -> float: + return self.c_get_mid_price() + + cdef object c_get_mid_price(self): + return self._market_info.get_mid_price() + + @property + def market_info_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: + return self._sb_order_tracker.market_pair_to_active_orders + + @property + def active_orders(self) -> List[LimitOrder]: + if self._market_info not in self.market_info_to_active_orders: + return [] + return self.market_info_to_active_orders[self._market_info] + + @property + def active_buys(self) -> List[LimitOrder]: + return [o for o in self.active_orders if o.is_buy] + + @property + def active_sells(self) -> List[LimitOrder]: + return [o for o in self.active_orders if not o.is_buy] + + @property + def logging_options(self) -> int: + return self._logging_options + + @logging_options.setter + def logging_options(self, int64_t logging_options): + self._logging_options = logging_options + + @property + def order_tracker(self): + return self._sb_order_tracker + + def pure_mm_assets_df(self, to_show_current_pct: bool) -> pd.DataFrame: + market, trading_pair, base_asset, quote_asset = self._market_info + price = self._market_info.get_mid_price() + base_balance = float(market.get_balance(base_asset)) + quote_balance = float(market.get_balance(quote_asset)) + available_base_balance = float(market.get_available_balance(base_asset)) + available_quote_balance = float(market.get_available_balance(quote_asset)) + base_value = base_balance * float(price) + total_in_quote = base_value + quote_balance + base_ratio = base_value / total_in_quote if total_in_quote > 0 else 0 + quote_ratio = quote_balance / total_in_quote if total_in_quote > 0 else 0 + data=[ + ["", base_asset, quote_asset], + ["Total Balance", round(base_balance, 4), round(quote_balance, 4)], + ["Available Balance", round(available_base_balance, 4), round(available_quote_balance, 4)], + [f"Current Value ({quote_asset})", round(base_value, 4), round(quote_balance, 4)] + ] + if to_show_current_pct: + data.append(["Current %", f"{base_ratio:.1%}", f"{quote_ratio:.1%}"]) + df = pd.DataFrame(data=data) + return df + + def active_orders_df(self) -> pd.DataFrame: + market, trading_pair, base_asset, quote_asset = self._market_info + price = self.get_price() + active_orders = self.active_orders + no_sells = len([o for o in active_orders if not o.is_buy and o.client_order_id]) + active_orders.sort(key=lambda x: x.price, reverse=True) + columns = ["Level", "Type", "Price", "Spread", "Amount (Orig)", "Amount (Adj)", "Age"] + data = [] + lvl_buy, lvl_sell = 0, 0 + for idx in range(0, len(active_orders)): + order = active_orders[idx] + spread = 0 if price == 0 else abs(order.price - price)/price + age = "n/a" + # // indicates order is a paper order so 'n/a'. For real orders, calculate age. + if "//" not in order.client_order_id: + age = pd.Timestamp(int(time.time()) - int(order.client_order_id[-16:])/1e6, + unit='s').strftime('%H:%M:%S') + amount_orig = self._order_amount + data.append([ + "", + "buy" if order.is_buy else "sell", + float(order.price), + f"{spread:.2%}", + amount_orig, + float(order.quantity), + age + ]) + + return pd.DataFrame(data=data, columns=columns) + + def market_status_data_frame(self, market_trading_pair_tuples: List[MarketTradingPairTuple]) -> pd.DataFrame: + markets_data = [] + markets_columns = ["Exchange", "Market", "Best Bid", "Best Ask", f"MidPrice"] + markets_columns.append('Reserved Price') + market_books = [(self._market_info.market, self._market_info.trading_pair)] + for market, trading_pair in market_books: + bid_price = market.get_price(trading_pair, False) + ask_price = market.get_price(trading_pair, True) + ref_price = self.get_price() + markets_data.append([ + market.display_name, + trading_pair, + float(bid_price), + float(ask_price), + float(ref_price), + round(self._reserved_price, 5), + ]) + return pd.DataFrame(data=markets_data, columns=markets_columns).replace(np.nan, '', regex=True) + + def format_status(self) -> str: + if not self._all_markets_ready: + return "Market connectors are not ready." + cdef: + list lines = [] + list warning_lines = [] + warning_lines.extend(self.network_warning([self._market_info])) + + markets_df = self.market_status_data_frame([self._market_info]) + lines.extend(["", " Markets:"] + [" " + line for line in markets_df.to_string(index=False).split("\n")]) + + assets_df = self.pure_mm_assets_df(True) + first_col_length = max(*assets_df[0].apply(len)) + df_lines = assets_df.to_string(index=False, header=False, + formatters={0: ("{:<" + str(first_col_length) + "}").format}).split("\n") + lines.extend(["", " Assets:"] + [" " + line for line in df_lines]) + + # See if there are any open orders. + if len(self.active_orders) > 0: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + else: + lines.extend(["", " No active maker orders."]) + + volatility_pct = self._avg_vol.current_value / float(self.get_price()) * 100.0 + if all((self._gamma, self._kappa, volatility_pct)): + lines.extend(["", f" Strategy parameters:", + f" risk_factor(\u03B3)= {self._gamma:.5E}", + f" order_book_depth_factor(\u03BA)= {self._kappa:.5E}", + f" volatility= {volatility_pct:.3f}%", + f" time until end of trading cycle= {str(datetime.timedelta(seconds=float(self._time_left)//1e3))}"]) + + warning_lines.extend(self.balance_warning([self._market_info])) + + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + + return "\n".join(lines) + + # The following exposed Python functions are meant for unit tests + # --------------------------------------------------------------- + def execute_orders_proposal(self, proposal: Proposal): + return self.c_execute_orders_proposal(proposal) + + def cancel_order(self, order_id: str): + return self.c_cancel_order(self._market_info, order_id) + + # --------------------------------------------------------------- + + cdef c_start(self, Clock clock, double timestamp): + StrategyBase.c_start(self, clock, timestamp) + self._last_timestamp = timestamp + # start tracking any restored limit order + restored_order_ids = self.c_track_restored_orders(self.market_info) + self._time_left = self._closing_time + + cdef c_tick(self, double timestamp): + StrategyBase.c_tick(self, timestamp) + cdef: + int64_t current_tick = (timestamp // self._status_report_interval) + int64_t last_tick = (self._last_timestamp // self._status_report_interval) + bint should_report_warnings = ((current_tick > last_tick) and + (self._logging_options & self.OPTION_LOG_STATUS_REPORT)) + cdef object proposal + try: + if not self._all_markets_ready: + self._all_markets_ready = all([mkt.ready for mkt in self._sb_markets]) + if not self._all_markets_ready: + # Markets not ready yet. Don't do anything. + if should_report_warnings: + self.logger().warning(f"Markets are not ready. No market making trades are permitted.") + return + + if should_report_warnings: + if not all([mkt.network_status is NetworkStatus.CONNECTED for mkt in self._sb_markets]): + self.logger().warning(f"WARNING: Some markets are not connected or are down at the moment. Market " + f"making may be dangerous when markets or networks are unstable.") + + self.c_collect_market_variables(timestamp) + if self.c_is_algorithm_ready(): + # If gamma or kappa are -1 then it's the first time they are calculated. + # Also, if volatility goes beyond the threshold specified, we consider volatility regime has changed + # so parameters need to be recalculated. + if (self._gamma is None) or (self._kappa is None) or \ + (self._parameters_based_on_spread and + self.volatility_diff_from_last_parameter_calculation(self.get_volatility()) > (self._vol_to_spread_multiplier - 1)): + self.c_recalculate_parameters() + self.c_calculate_reserved_price_and_optimal_spread() + + proposal = None + if self._create_timestamp <= self._current_timestamp: + # 1. Create base order proposals + proposal = self.c_create_base_proposal() + # 2. Apply functions that modify orders amount + self.c_apply_order_amount_eta_transformation(proposal) + # 3. Apply functions that modify orders price + self.c_apply_order_price_modifiers(proposal) + # 4. Apply budget constraint, i.e. can't buy/sell more than what you have. + self.c_apply_budget_constraint(proposal) + + self.c_cancel_active_orders(proposal) + if self._is_debug: + self.dump_debug_variables() + refresh_proposal = self.c_aged_order_refresh() + # Firstly restore cancelled aged order + if refresh_proposal is not None: + self.c_execute_orders_proposal(refresh_proposal) + if self.c_to_create_orders(proposal): + self.c_execute_orders_proposal(proposal) + else: + self._ticks_to_be_ready-=1 + if self._ticks_to_be_ready % 5 == 0: + self.logger().info(f"Calculating volatility... {self._ticks_to_be_ready} seconds to start trading") + finally: + self._last_timestamp = timestamp + + cdef c_collect_market_variables(self, double timestamp): + market, trading_pair, base_asset, quote_asset = self._market_info + self._last_sampling_timestamp = timestamp + self._time_left = max(self._time_left - Decimal(timestamp - self._last_timestamp) * 1000, 0) + price = self.get_price() + self._avg_vol.add_sample(price) + # Calculate adjustment factor to have 0.01% of inventory resolution + base_balance = market.get_balance(base_asset) + quote_balance = market.get_balance(quote_asset) + inventory_in_base = quote_balance / price + base_balance + self._q_adjustment_factor = Decimal( + "1e5") / inventory_in_base + if self._time_left == 0: + # Re-cycle algorithm + self._time_left = self._closing_time + if self._parameters_based_on_spread: + self.c_recalculate_parameters() + self.logger().info("Recycling algorithm time left and parameters if needed.") + + def volatility_diff_from_last_parameter_calculation(self, current_vol): + if self._latest_parameter_calculation_vol == 0: + return s_decimal_zero + return abs(self._latest_parameter_calculation_vol - Decimal(str(current_vol))) / self._latest_parameter_calculation_vol + + cdef double c_get_spread(self): + cdef: + ExchangeBase market = self._market_info.market + str trading_pair = self._market_info.trading_pair + + return market.c_get_price(trading_pair, True) - market.c_get_price(trading_pair, False) + + def get_volatility(self): + vol = Decimal(str(self._avg_vol.current_value)) + if vol == s_decimal_zero: + if self._latest_parameter_calculation_vol != s_decimal_zero: + vol = Decimal(str(self._latest_parameter_calculation_vol)) + else: + # Default value at start time if price has no activity + vol = Decimal(str(self.c_get_spread()/2)) + return vol + + cdef c_calculate_reserved_price_and_optimal_spread(self): + cdef: + ExchangeBase market = self._market_info.market + + time_left_fraction = Decimal(str(self._time_left / self._closing_time)) + + price = self.get_price() + q = (market.get_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory()))) * self._q_adjustment_factor + vol = self.get_volatility() + mid_price_variance = vol ** 2 + + self._reserved_price = price - (q * self._gamma * mid_price_variance * time_left_fraction) + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal( + 1 + self._gamma / self._kappa).ln() / self._gamma + + if self._parameters_based_on_spread: + min_limit_bid = min(price * (1 - self._max_spread), price - self._vol_to_spread_multiplier * vol) + max_limit_bid = price * (1 - self._min_spread) + min_limit_ask = price * (1 + self._min_spread) + max_limit_ask = max(price * (1 + self._max_spread), price + self._vol_to_spread_multiplier * vol) + else: + min_limit_bid = s_decimal_zero + max_limit_bid = min_limit_ask = price + max_limit_ask = Decimal("Inf") + + self._optimal_ask = min(max(self._reserved_price + self._optimal_spread / 2, + min_limit_ask), + max_limit_ask) + self._optimal_bid = min(max(self._reserved_price - self._optimal_spread / 2, + min_limit_bid), + max_limit_bid) + # This is not what the algorithm will use as proposed bid and ask. This is just the raw output. + # Optimal bid and optimal ask prices will be used + if self._is_debug: + self.logger().info(f"bid={(price-(self._reserved_price - self._optimal_spread / 2)) / price * 100:.4f}% | " + f"ask={((self._reserved_price + self._optimal_spread / 2) - price) / price * 100:.4f}% | " + f"q={q/self._q_adjustment_factor:.4f} | " + f"vol={vol:.4f}") + + cdef object c_calculate_target_inventory(self): + cdef: + ExchangeBase market = self._market_info.market + str trading_pair = self._market_info.trading_pair + str base_asset = self._market_info.base_asset + str quote_asset = self._market_info.quote_asset + object mid_price + object base_value + object inventory_value + object target_inventory_value + + price = self.get_price() + base_asset_amount = market.get_balance(base_asset) + quote_asset_amount = market.get_balance(quote_asset) + base_value = base_asset_amount * price + inventory_value = base_value + quote_asset_amount + target_inventory_value = inventory_value * self._inventory_target_base_pct + return market.c_quantize_order_amount(trading_pair, Decimal(str(target_inventory_value / price))) + + cdef c_recalculate_parameters(self): + cdef: + ExchangeBase market = self._market_info.market + + q = (market.get_balance(self.base_asset) - self.c_calculate_target_inventory()) * self._q_adjustment_factor + vol = self.get_volatility() + price=self.get_price() + + if q != 0: + min_spread = self._min_spread * price + max_spread = self._max_spread * price + + # GAMMA + # If q or vol are close to 0, gamma will -> Inf. Is this desirable? + max_possible_gamma = min( + (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)), + (max_spread * (2-self._inventory_risk_aversion) / + self._inventory_risk_aversion + min_spread) / (vol ** 2)) + self._gamma = self._inventory_risk_aversion * max_possible_gamma + + # KAPPA + # Want the maximum possible spread but with restrictions to avoid negative kappa or division by 0 + max_spread_around_reserved_price = max_spread * (2-self._inventory_risk_aversion) + min_spread * self._inventory_risk_aversion + if max_spread_around_reserved_price <= self._gamma * (vol ** 2): + self._kappa = Decimal('1e100') # Cap to kappa -> Infinity + else: + self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) + + # ETA + + q_where_to_decay_order_amount = self.c_calculate_target_inventory() * (1 - self._inventory_risk_aversion) + self._eta = s_decimal_one + if q_where_to_decay_order_amount != s_decimal_zero: + self._eta = self._eta / q_where_to_decay_order_amount + + self._latest_parameter_calculation_vol = vol + + cdef bint c_is_algorithm_ready(self): + return self._avg_vol.is_sampling_buffer_full + + cdef object c_create_base_proposal(self): + cdef: + ExchangeBase market = self._market_info.market + list buys = [] + list sells = [] + + price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_bid))) + size = market.c_quantize_order_amount(self.trading_pair, self._order_amount) + if size>0: + buys.append(PriceSize(price, size)) + + price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_ask))) + size = market.c_quantize_order_amount(self.trading_pair, self._order_amount) + if size>0: + sells.append(PriceSize(price, size)) + + return Proposal(buys, sells) + + cdef tuple c_get_adjusted_available_balance(self, list orders): + """ + Calculates the available balance, plus the amount attributed to orders. + :return: (base amount, quote amount) in Decimal + """ + cdef: + ExchangeBase market = self._market_info.market + object base_balance = market.c_get_available_balance(self.base_asset) + object quote_balance = market.c_get_available_balance(self.quote_asset) + + for order in orders: + if order.is_buy: + quote_balance += order.quantity * order.price + else: + base_balance += order.quantity + + return base_balance, quote_balance + + cdef c_apply_order_price_modifiers(self, object proposal): + if self._order_optimization_enabled: + self.c_apply_order_optimization(proposal) + + if self._add_transaction_costs_to_orders: + self.c_apply_add_transaction_costs(proposal) + + cdef c_apply_budget_constraint(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + object quote_size + object base_size + object adjusted_amount + + base_balance, quote_balance = self.c_get_adjusted_available_balance(self.active_orders) + + for buy in proposal.buys: + buy_fee = market.c_get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, + buy.size, buy.price) + quote_size = buy.size * buy.price * (Decimal(1) + buy_fee.percent) + + # Adjust buy order size to use remaining balance if less than the order amount + if quote_balance < quote_size: + adjusted_amount = quote_balance / (buy.price * (Decimal("1") + buy_fee.percent)) + adjusted_amount = market.c_quantize_order_amount(self.trading_pair, adjusted_amount) + buy.size = adjusted_amount + quote_balance = s_decimal_zero + elif quote_balance == s_decimal_zero: + buy.size = s_decimal_zero + else: + quote_balance -= quote_size + + proposal.buys = [o for o in proposal.buys if o.size > 0] + + for sell in proposal.sells: + base_size = sell.size + + # Adjust sell order size to use remaining balance if less than the order amount + if base_balance < base_size: + adjusted_amount = market.c_quantize_order_amount(self.trading_pair, base_balance) + sell.size = adjusted_amount + base_balance = s_decimal_zero + elif base_balance == s_decimal_zero: + sell.size = s_decimal_zero + else: + base_balance -= base_size + + proposal.sells = [o for o in proposal.sells if o.size > 0] + + # Compare the market price with the top bid and top ask price + cdef c_apply_order_optimization(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + object own_buy_size = s_decimal_zero + object own_sell_size = s_decimal_zero + object best_order_spread + + for order in self.active_orders: + if order.is_buy: + own_buy_size = order.quantity + else: + own_sell_size = order.quantity + + if len(proposal.buys) > 0: + # Get the top bid price in the market using order_optimization_depth and your buy order volume + top_bid_price = self._market_info.get_price_for_volume( + False, own_buy_size).result_price + price_quantum = market.c_get_order_price_quantum( + self.trading_pair, + top_bid_price + ) + # Get the price above the top bid + price_above_bid = (ceil(top_bid_price / price_quantum) + 1) * price_quantum + + # If the price_above_bid is lower than the price suggested by the top pricing proposal, + # lower the price and from there apply the best_order_spread to each order in the next levels + proposal.buys = sorted(proposal.buys, key = lambda p: p.price, reverse = True) + lower_buy_price = min(proposal.buys[0].price, price_above_bid) + for i, proposed in enumerate(proposal.buys): + proposal.buys[i].price = market.c_quantize_order_price(self.trading_pair, lower_buy_price) + + if len(proposal.sells) > 0: + # Get the top ask price in the market using order_optimization_depth and your sell order volume + top_ask_price = self._market_info.get_price_for_volume( + True, own_sell_size).result_price + price_quantum = market.c_get_order_price_quantum( + self.trading_pair, + top_ask_price + ) + # Get the price below the top ask + price_below_ask = (floor(top_ask_price / price_quantum) - 1) * price_quantum + + # If the price_below_ask is higher than the price suggested by the pricing proposal, + # increase your price and from there apply the best_order_spread to each order in the next levels + proposal.sells = sorted(proposal.sells, key = lambda p: p.price) + higher_sell_price = max(proposal.sells[0].price, price_below_ask) + for i, proposed in enumerate(proposal.sells): + proposal.sells[i].price = market.c_quantize_order_price(self.trading_pair, higher_sell_price) + + cdef c_apply_order_amount_eta_transformation(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + str trading_pair = self._market_info.trading_pair + + # eta parameter is described in the paper as the shape parameter for having exponentially decreasing order amount + # for orders that go against inventory target (i.e. Want to buy when excess inventory or sell when deficit inventory) + q = market.get_balance(self.base_asset) - self.c_calculate_target_inventory() + if len(proposal.buys) > 0: + if q > 0: + for i, proposed in enumerate(proposal.buys): + + proposal.buys[i].size = market.c_quantize_order_amount(trading_pair, proposal.buys[i].size * Decimal.exp(-self._eta * q)) + proposal.buys = [o for o in proposal.buys if o.size > 0] + + if len(proposal.sells) > 0: + if q < 0: + for i, proposed in enumerate(proposal.sells): + proposal.sells[i].size = market.c_quantize_order_amount(trading_pair, proposal.sells[i].size * Decimal.exp(self._eta * q)) + proposal.sells = [o for o in proposal.sells if o.size > 0] + + cdef object c_apply_add_transaction_costs(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + for buy in proposal.buys: + fee = market.c_get_fee(self.base_asset, self.quote_asset, + self._limit_order_type, TradeType.BUY, buy.size, buy.price) + price = buy.price * (Decimal(1) - fee.percent) + buy.price = market.c_quantize_order_price(self.trading_pair, price) + for sell in proposal.sells: + fee = market.c_get_fee(self.base_asset, self.quote_asset, + self._limit_order_type, TradeType.SELL, sell.size, sell.price) + price = sell.price * (Decimal(1) + fee.percent) + sell.price = market.c_quantize_order_price(self.trading_pair, price) + + cdef c_did_fill_order(self, object order_filled_event): + cdef: + str order_id = order_filled_event.order_id + object market_info = self._sb_order_tracker.c_get_shadow_market_pair_from_order_id(order_id) + tuple order_fill_record + + if market_info is not None: + limit_order_record = self._sb_order_tracker.c_get_shadow_limit_order(order_id) + order_fill_record = (limit_order_record, order_filled_event) + + if order_filled_event.trade_type is TradeType.BUY: + if self._logging_options & self.OPTION_LOG_MAKER_ORDER_FILLED: + self.log_with_clock( + logging.INFO, + f"({market_info.trading_pair}) Maker buy order of " + f"{order_filled_event.amount} {market_info.base_asset} filled." + ) + else: + if self._logging_options & self.OPTION_LOG_MAKER_ORDER_FILLED: + self.log_with_clock( + logging.INFO, + f"({market_info.trading_pair}) Maker sell order of " + f"{order_filled_event.amount} {market_info.base_asset} filled." + ) + + cdef c_did_complete_buy_order(self, object order_completed_event): + cdef: + str order_id = order_completed_event.order_id + limit_order_record = self._sb_order_tracker.c_get_limit_order(self._market_info, order_id) + if limit_order_record is None: + return + active_sell_ids = [x.client_order_id for x in self.active_orders if not x.is_buy] + + # delay order creation by filled_order_delay (in seconds) + self._create_timestamp = self._current_timestamp + self._filled_order_delay + self._cancel_timestamp = min(self._cancel_timestamp, self._create_timestamp) + + self._filled_buys_balance += 1 + self._last_own_trade_price = limit_order_record.price + + self.log_with_clock( + logging.INFO, + f"({self.trading_pair}) Maker buy order {order_id} " + f"({limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency}) has been completely filled." + ) + self.notify_hb_app( + f"Maker BUY order {limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." + ) + + cdef c_did_complete_sell_order(self, object order_completed_event): + cdef: + str order_id = order_completed_event.order_id + LimitOrder limit_order_record = self._sb_order_tracker.c_get_limit_order(self._market_info, order_id) + if limit_order_record is None: + return + active_buy_ids = [x.client_order_id for x in self.active_orders if x.is_buy] + + # delay order creation by filled_order_delay (in seconds) + self._create_timestamp = self._current_timestamp + self._filled_order_delay + self._cancel_timestamp = min(self._cancel_timestamp, self._create_timestamp) + + self._filled_sells_balance += 1 + self._last_own_trade_price = limit_order_record.price + + self.log_with_clock( + logging.INFO, + f"({self.trading_pair}) Maker sell order {order_id} " + f"({limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency}) has been completely filled." + ) + self.notify_hb_app( + f"Maker SELL order {limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." + ) + + cdef bint c_is_within_tolerance(self, list current_prices, list proposal_prices): + if len(current_prices) != len(proposal_prices): + return False + current_prices = sorted(current_prices) + proposal_prices = sorted(proposal_prices) + for current, proposal in zip(current_prices, proposal_prices): + # if spread diff is more than the tolerance or order quantities are different, return false. + if abs(proposal - current)/current > self._order_refresh_tolerance_pct: + return False + return True + + # Cancel active orders + # Return value: whether order cancellation is deferred. + cdef c_cancel_active_orders(self, object proposal): + if self._cancel_timestamp > self._current_timestamp: + return + if not global_config_map.get("0x_active_cancels").value: + if ((self._market_info.market.name in self.RADAR_RELAY_TYPE_EXCHANGES) or + (self._market_info.market.name == "bamboo_relay" and not self._market_info.market.use_coordinator)): + return + + cdef: + list active_orders = self.active_orders + list active_buy_prices = [] + list active_sells = [] + bint to_defer_canceling = False + if len(active_orders) == 0: + return + if proposal is not None: + active_buy_prices = [Decimal(str(o.price)) for o in active_orders if o.is_buy] + active_sell_prices = [Decimal(str(o.price)) for o in active_orders if not o.is_buy] + proposal_buys = [buy.price for buy in proposal.buys] + proposal_sells = [sell.price for sell in proposal.sells] + if self.c_is_within_tolerance(active_buy_prices, proposal_buys) and \ + self.c_is_within_tolerance(active_sell_prices, proposal_sells): + to_defer_canceling = True + + if not to_defer_canceling: + for order in active_orders: + self.c_cancel_order(self._market_info, order.client_order_id) + else: + self.set_timers() + + # Refresh all active order that are older that the _max_order_age + cdef c_aged_order_refresh(self): + cdef: + list active_orders = self.active_orders + list buys = [] + list sells = [] + + for order in active_orders: + age = 0 if "//" in order.client_order_id else \ + int(int(time.time()) - int(order.client_order_id[-16:])/1e6) + + # To prevent duplicating orders due to delay in receiving cancel response + refresh_check = [o for o in active_orders if o.price == order.price + and o.quantity == order.quantity] + if len(refresh_check) > 1: + continue + + if age >= self._max_order_age: + if order.is_buy: + buys.append(PriceSize(order.price, order.quantity)) + else: + sells.append(PriceSize(order.price, order.quantity)) + self.logger().info(f"Refreshing {'Buy' if order.is_buy else 'Sell'} order with ID - " + f"{order.client_order_id} because it reached maximum order age of " + f"{self._max_order_age} seconds.") + self.c_cancel_order(self._market_info, order.client_order_id) + return Proposal(buys, sells) + + cdef bint c_to_create_orders(self, object proposal): + return self._create_timestamp < self._current_timestamp and \ + proposal is not None + + cdef c_execute_orders_proposal(self, object proposal): + cdef: + double expiration_seconds = (self._order_refresh_time + if ((self._market_info.market.name in self.RADAR_RELAY_TYPE_EXCHANGES) or + (self._market_info.market.name == "bamboo_relay" and + not self._market_info.market.use_coordinator)) + else NaN) + str bid_order_id, ask_order_id + bint orders_created = False + + if len(proposal.buys) > 0: + if self._logging_options & self.OPTION_LOG_CREATE_ORDER: + price_quote_str = [f"{buy.size.normalize()} {self.base_asset}, " + f"{buy.price.normalize()} {self.quote_asset}" + for buy in proposal.buys] + self.logger().info( + f"({self.trading_pair}) Creating {len(proposal.buys)} bid orders " + f"at (Size, Price): {price_quote_str}" + ) + for buy in proposal.buys: + bid_order_id = self.c_buy_with_specific_market( + self._market_info, + buy.size, + order_type=self._limit_order_type, + price=buy.price, + expiration_seconds=expiration_seconds + ) + orders_created = True + if len(proposal.sells) > 0: + if self._logging_options & self.OPTION_LOG_CREATE_ORDER: + price_quote_str = [f"{sell.size.normalize()} {self.base_asset}, " + f"{sell.price.normalize()} {self.quote_asset}" + for sell in proposal.sells] + self.logger().info( + f"({self.trading_pair}) Creating {len(proposal.sells)} ask " + f"orders at (Size, Price): {price_quote_str}" + ) + for sell in proposal.sells: + ask_order_id = self.c_sell_with_specific_market( + self._market_info, + sell.size, + order_type=self._limit_order_type, + price=sell.price, + expiration_seconds=expiration_seconds + ) + orders_created = True + if orders_created: + self.set_timers() + + cdef set_timers(self): + cdef double next_cycle = self._current_timestamp + self._order_refresh_time + if self._create_timestamp <= self._current_timestamp: + self._create_timestamp = next_cycle + if self._cancel_timestamp <= self._current_timestamp: + self._cancel_timestamp = min(self._create_timestamp, next_cycle) + + def notify_hb_app(self, msg: str): + if self._hb_app_notification: + from hummingbot.client.hummingbot_application import HummingbotApplication + HummingbotApplication.main_application()._notify(msg) + + def dump_debug_variables(self): + market = self._market_info.market + mid_price = self.get_price() + spread = Decimal(str(self.c_get_spread())) + + best_ask = mid_price + spread / 2 + new_ask = self._reserved_price + self._optimal_spread / 2 + best_bid = mid_price - spread / 2 + new_bid = self._reserved_price - self._optimal_spread / 2 + if not os.path.exists(self._debug_csv_path): + df_header = pd.DataFrame([('mid_price', + 'spread', + 'reserved_price', + 'optimal_spread', + 'optimal_bid', + 'optimal_ask', + 'optimal_bid_to_mid_%', + 'optimal_ask_to_mid_%', + 'current_inv', + 'target_inv', + 'time_left_fraction', + 'mid_price std_dev', + 'gamma', + 'kappa', + 'eta', + 'current_vol_to_calculation_vol', + 'inventory_target_pct', + 'min_spread', + 'max_spread', + 'vol_to_spread_multiplier')]) + df_header.to_csv(self._debug_csv_path, mode='a', header=False, index=False) + df = pd.DataFrame([(mid_price, + spread, + self._reserved_price, + self._optimal_spread, + self._optimal_bid, + self._optimal_ask, + (mid_price - (self._reserved_price - self._optimal_spread / 2)) / mid_price, + ((self._reserved_price + self._optimal_spread / 2) - mid_price) / mid_price, + market.get_balance(self.base_asset), + self.c_calculate_target_inventory(), + self._time_left / self._closing_time, + self._avg_vol.current_value, + self._gamma, + self._kappa, + self._eta, + self.volatility_diff_from_last_parameter_calculation(self.get_volatility()), + self.inventory_target_base_pct, + self._min_spread, + self._max_spread, + self._vol_to_spread_multiplier)]) + df.to_csv(self._debug_csv_path, mode='a', header=False, index=False) diff --git a/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py new file mode 100644 index 0000000000..34a6617163 --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py @@ -0,0 +1,243 @@ +from decimal import Decimal + +from hummingbot.client.config.config_var import ConfigVar +from hummingbot.client.config.config_validators import ( + validate_exchange, + validate_market_trading_pair, + validate_bool, + validate_decimal, +) +from hummingbot.client.settings import ( + required_exchanges, + EXAMPLE_PAIRS, +) +from hummingbot.client.config.global_config_map import ( + using_bamboo_coordinator_mode, + using_exchange +) +from hummingbot.client.config.config_helpers import ( + minimum_order_amount, +) +from typing import Optional + + +def maker_trading_pair_prompt(): + exchange = avellaneda_market_making_config_map.get("exchange").value + example = EXAMPLE_PAIRS.get(exchange) + return "Enter the token trading pair you would like to trade on %s%s >>> " \ + % (exchange, f" (e.g. {example})" if example else "") + + +# strategy specific validators +def validate_exchange_trading_pair(value: str) -> Optional[str]: + exchange = avellaneda_market_making_config_map.get("exchange").value + return validate_market_trading_pair(exchange, value) + + +def validate_max_spread(value: str) -> Optional[str]: + validate_decimal(value, 0, 100, inclusive=False) + if avellaneda_market_making_config_map["min_spread"].value is not None: + min_spread = Decimal(avellaneda_market_making_config_map["min_spread"].value) + max_spread = Decimal(value) + if min_spread >= max_spread: + return f"Max spread cannot be lesser or equal to min spread {max_spread}%<={min_spread}%" + + +def onvalidated_min_spread(value: str): + # If entered valid min_spread, max_spread is invalidated so user sets it up again + avellaneda_market_making_config_map["max_spread"].value = None + + +async def order_amount_prompt() -> str: + exchange = avellaneda_market_making_config_map["exchange"].value + trading_pair = avellaneda_market_making_config_map["market"].value + base_asset, quote_asset = trading_pair.split("-") + min_amount = await minimum_order_amount(exchange, trading_pair) + return f"What is the amount of {base_asset} per order? (minimum {min_amount}) >>> " + + +async def validate_order_amount(value: str) -> Optional[str]: + try: + exchange = avellaneda_market_making_config_map["exchange"].value + trading_pair = avellaneda_market_making_config_map["market"].value + min_amount = await minimum_order_amount(exchange, trading_pair) + if Decimal(value) < min_amount: + return f"Order amount must be at least {min_amount}." + except Exception: + return "Invalid order amount." + + +def on_validated_price_source_exchange(value: str): + if value is None: + avellaneda_market_making_config_map["price_source_market"].value = None + + +def exchange_on_validated(value: str): + required_exchanges.append(value) + + +def on_validated_parameters_based_on_spread(value: str): + if value == 'True': + avellaneda_market_making_config_map.get("risk_factor").value = None + avellaneda_market_making_config_map.get("order_book_depth_factor").value = None + avellaneda_market_making_config_map.get("order_amount_shape_factor").value = None + else: + avellaneda_market_making_config_map.get("max_spread").value = None + avellaneda_market_making_config_map.get("min_spread").value = None + avellaneda_market_making_config_map.get("vol_to_spread_multiplier").value = None + avellaneda_market_making_config_map.get("inventory_risk_aversion").value = None + + +avellaneda_market_making_config_map = { + "strategy": + ConfigVar(key="strategy", + prompt=None, + default="avellaneda_market_making"), + "exchange": + ConfigVar(key="exchange", + prompt="Enter your maker spot connector >>> ", + validator=validate_exchange, + on_validated=exchange_on_validated, + prompt_on_new=True), + "market": + ConfigVar(key="market", + prompt=maker_trading_pair_prompt, + validator=validate_exchange_trading_pair, + prompt_on_new=True), + "order_amount": + ConfigVar(key="order_amount", + prompt=order_amount_prompt, + type_str="decimal", + validator=validate_order_amount, + prompt_on_new=True), + "order_optimization_enabled": + ConfigVar(key="order_optimization_enabled", + prompt="Do you want to enable best bid ask jumping? (Yes/No) >>> ", + type_str="bool", + default=True, + validator=validate_bool), + "parameters_based_on_spread": + ConfigVar(key="parameters_based_on_spread", + prompt="Do you want to automate Avellaneda-Stoikov parameters based on min/max spread? >>> ", + type_str="bool", + validator=validate_bool, + on_validated=on_validated_parameters_based_on_spread, + default=True, + prompt_on_new=True), + "min_spread": + ConfigVar(key="min_spread", + prompt="Enter the minimum spread allowed from mid-price in percentage " + "(Enter 1 to indicate 1%) >>> ", + type_str="decimal", + required_if=lambda: avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True, + on_validated=onvalidated_min_spread), + "max_spread": + ConfigVar(key="max_spread", + prompt="Enter the maximum spread allowed from mid-price in percentage " + "(Enter 1 to indicate 1%) >>> ", + type_str="decimal", + required_if=lambda: avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_max_spread(v), + prompt_on_new=True), + "vol_to_spread_multiplier": + ConfigVar(key="vol_to_spread_multiplier", + prompt="Enter the Volatility threshold multiplier (Should be greater than 1.0): " + "(If market volatility multiplied by this value is above the maximum spread, it will increase the maximum spread value) >>>", + type_str="decimal", + required_if=lambda: avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 1, 10, inclusive=False), + prompt_on_new=True), + "inventory_risk_aversion": + ConfigVar(key="inventory_risk_aversion", + prompt="Enter Inventory risk aversion between 0 and 1: (For values close to 0.999 spreads will be more " + "skewed to meet the inventory target, while close to 0.001 spreads will be close to symmetrical, " + "increasing profitability but also increasing inventory risk)>>>", + type_str="decimal", + required_if=lambda: avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), + prompt_on_new=True), + "order_book_depth_factor": + ConfigVar(key="order_book_depth_factor", + printable_key="order_book_depth_factor(\u03BA)", + prompt="Enter order book depth factor (\u03BA) >>> ", + type_str="decimal", + required_if=lambda: not avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), + prompt_on_new=True), + "risk_factor": + ConfigVar(key="risk_factor", + printable_key="risk_factor(\u03B3)", + prompt="Enter risk factor (\u03B3) >>> ", + type_str="decimal", + required_if=lambda: not avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), + prompt_on_new=True), + "order_amount_shape_factor": + ConfigVar(key="order_amount_shape_factor", + printable_key="order_amount_shape_factor(\u03B7)", + prompt="Enter order amount shape factor (\u03B7) >>> ", + type_str="decimal", + required_if=lambda: not avellaneda_market_making_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1, inclusive=True), + prompt_on_new=True), + "closing_time": + ConfigVar(key="closing_time", + prompt="Enter operational closing time (T). (How long will each trading cycle last " + "in days or fractions of day) >>> ", + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), + default=Decimal("0.041666667")), + "order_refresh_time": + ConfigVar(key="order_refresh_time", + prompt="How often do you want to cancel and replace bids and asks " + "(in seconds)? >>> ", + required_if=lambda: not (using_exchange("radar_relay")() or + (using_exchange("bamboo_relay")() and not using_bamboo_coordinator_mode())), + type_str="float", + validator=lambda v: validate_decimal(v, 0, inclusive=False), + prompt_on_new=True), + "max_order_age": + ConfigVar(key="max_order_age", + prompt="How long do you want to cancel and replace bids and asks " + "with the same price (in seconds)? >>> ", + required_if=lambda: not (using_exchange("radar_relay")() or + (using_exchange("bamboo_relay")() and not using_bamboo_coordinator_mode())), + type_str="float", + default=Decimal("1800"), + validator=lambda v: validate_decimal(v, 0, inclusive=False)), + "order_refresh_tolerance_pct": + ConfigVar(key="order_refresh_tolerance_pct", + prompt="Enter the percent change in price needed to refresh orders at each cycle " + "(Enter 1 to indicate 1%) >>> ", + type_str="decimal", + default=Decimal("0"), + validator=lambda v: validate_decimal(v, -10, 10, inclusive=True)), + "filled_order_delay": + ConfigVar(key="filled_order_delay", + prompt="How long do you want to wait before placing the next order " + "if your order gets filled (in seconds)? >>> ", + type_str="float", + validator=lambda v: validate_decimal(v, min_value=0, inclusive=False), + default=60), + "inventory_target_base_pct": + ConfigVar(key="inventory_target_base_pct", + prompt="What is the inventory target for the base asset? Enter 50 for 50% >>> ", + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100), + prompt_on_new=True, + default=Decimal("50")), + "add_transaction_costs": + ConfigVar(key="add_transaction_costs", + prompt="Do you want to add transaction costs automatically to order prices? (Yes/No) >>> ", + type_str="bool", + default=False, + validator=validate_bool), + "volatility_buffer_size": + ConfigVar(key="volatility_buffer_size", + prompt="Enter amount of ticks that will be stored to calculate volatility>>> ", + type_str="int", + validator=lambda v: validate_decimal(v, 5, 600), + default=60), +} diff --git a/hummingbot/strategy/avellaneda_market_making/data_types.py b/hummingbot/strategy/avellaneda_market_making/data_types.py new file mode 100644 index 0000000000..ce86cd6091 --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/data_types.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +from typing import ( + NamedTuple, + List +) +from decimal import Decimal +from hummingbot.core.event.events import OrderType + +ORDER_PROPOSAL_ACTION_CREATE_ORDERS = 1 +ORDER_PROPOSAL_ACTION_CANCEL_ORDERS = 1 << 1 + + +class OrdersProposal(NamedTuple): + actions: int + buy_order_type: OrderType + buy_order_prices: List[Decimal] + buy_order_sizes: List[Decimal] + sell_order_type: OrderType + sell_order_prices: List[Decimal] + sell_order_sizes: List[Decimal] + cancel_order_ids: List[str] + + +class PricingProposal(NamedTuple): + buy_order_prices: List[Decimal] + sell_order_prices: List[Decimal] + + +class SizingProposal(NamedTuple): + buy_order_sizes: List[Decimal] + sell_order_sizes: List[Decimal] + + +class PriceSize: + def __init__(self, price: Decimal, size: Decimal): + self.price: Decimal = price + self.size: Decimal = size + + def __repr__(self): + return f"[ p: {self.price} s: {self.size} ]" + + +class Proposal: + def __init__(self, buys: List[PriceSize], sells: List[PriceSize]): + self.buys: List[PriceSize] = buys + self.sells: List[PriceSize] = sells + + def __repr__(self): + return f"{len(self.buys)} buys: {', '.join([str(o) for o in self.buys])} " \ + f"{len(self.sells)} sells: {', '.join([str(o) for o in self.sells])}" diff --git a/hummingbot/strategy/avellaneda_market_making/start.py b/hummingbot/strategy/avellaneda_market_making/start.py new file mode 100644 index 0000000000..2d12fe8a39 --- /dev/null +++ b/hummingbot/strategy/avellaneda_market_making/start.py @@ -0,0 +1,85 @@ +from typing import ( + List, + Tuple, +) + +from hummingbot import data_path +import os.path +from hummingbot.client.hummingbot_application import HummingbotApplication +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.avellaneda_market_making import ( + AvellanedaMarketMakingStrategy, +) +from hummingbot.strategy.avellaneda_market_making.avellaneda_market_making_config_map import avellaneda_market_making_config_map as c_map +from decimal import Decimal +import pandas as pd + + +def start(self): + try: + order_amount = c_map.get("order_amount").value + order_optimization_enabled = c_map.get("order_optimization_enabled").value + order_refresh_time = c_map.get("order_refresh_time").value + exchange = c_map.get("exchange").value.lower() + raw_trading_pair = c_map.get("market").value + inventory_target_base_pct = 0 if c_map.get("inventory_target_base_pct").value is None else \ + c_map.get("inventory_target_base_pct").value / Decimal('100') + filled_order_delay = c_map.get("filled_order_delay").value + order_refresh_tolerance_pct = c_map.get("order_refresh_tolerance_pct").value / Decimal('100') + add_transaction_costs_to_orders = c_map.get("add_transaction_costs").value + + trading_pair: str = raw_trading_pair + maker_assets: Tuple[str, str] = self._initialize_market_assets(exchange, [trading_pair])[0] + market_names: List[Tuple[str, List[str]]] = [(exchange, [trading_pair])] + self._initialize_wallet(token_trading_pairs=list(set(maker_assets))) + self._initialize_markets(market_names) + self.assets = set(maker_assets) + maker_data = [self.markets[exchange], trading_pair] + list(maker_assets) + self.market_trading_pair_tuples = [MarketTradingPairTuple(*maker_data)] + + strategy_logging_options = AvellanedaMarketMakingStrategy.OPTION_LOG_ALL + parameters_based_on_spread = c_map.get("parameters_based_on_spread").value + if parameters_based_on_spread: + risk_factor = order_book_depth_factor = order_amount_shape_factor = None + min_spread = c_map.get("min_spread").value / Decimal(100) + max_spread = c_map.get("max_spread").value / Decimal(100) + vol_to_spread_multiplier = c_map.get("vol_to_spread_multiplier").value + inventory_risk_aversion = c_map.get("inventory_risk_aversion").value + else: + min_spread = max_spread = vol_to_spread_multiplier = inventory_risk_aversion = None + order_book_depth_factor = c_map.get("order_book_depth_factor").value + risk_factor = c_map.get("risk_factor").value + order_amount_shape_factor = c_map.get("order_amount_shape_factor").value + closing_time = c_map.get("closing_time").value * Decimal(3600 * 24 * 1e3) + volatility_buffer_size = c_map.get("volatility_buffer_size").value + debug_csv_path = os.path.join(data_path(), + HummingbotApplication.main_application().strategy_file_name.rsplit('.', 1)[0] + + f"_{pd.Timestamp.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv") + + self.strategy = AvellanedaMarketMakingStrategy( + market_info=MarketTradingPairTuple(*maker_data), + order_amount=order_amount, + order_optimization_enabled=order_optimization_enabled, + inventory_target_base_pct=inventory_target_base_pct, + order_refresh_time=order_refresh_time, + order_refresh_tolerance_pct=order_refresh_tolerance_pct, + filled_order_delay=filled_order_delay, + add_transaction_costs_to_orders=add_transaction_costs_to_orders, + logging_options=strategy_logging_options, + hb_app_notification=True, + parameters_based_on_spread=parameters_based_on_spread, + min_spread=min_spread, + max_spread=max_spread, + vol_to_spread_multiplier=vol_to_spread_multiplier, + inventory_risk_aversion=inventory_risk_aversion, + order_book_depth_factor=order_book_depth_factor, + risk_factor=risk_factor, + order_amount_shape_factor=order_amount_shape_factor, + closing_time=closing_time, + debug_csv_path=debug_csv_path, + volatility_buffer_size=volatility_buffer_size, + is_debug=False + ) + except Exception as e: + self._notify(str(e)) + self.logger().error("Unknown error during initialization.", exc_info=True) diff --git a/hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml new file mode 100644 index 0000000000..c4205052b4 --- /dev/null +++ b/hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml @@ -0,0 +1,52 @@ +######################################################## +### Avellaneda market making strategy config ### +######################################################## + +template_version: 1 +strategy: null + +# Exchange and token parameters. +exchange: null + +# Token trading pair for the exchange, e.g. BTC-USDT +market: null + +# Time in seconds before cancelling and placing new orders. +# If the value is 60, the bot cancels active orders and placing new ones after a minute. +order_refresh_time: null + +# Whether to enable order optimization mode (true/false). +order_optimization_enabled: true + +# Time in seconds before replacing existing order with new orders at thesame price. +max_order_age: null + +# The spread (from mid price) to defer order refresh process to the next cycle. +# (Enter 1 to indicate 1%), value below 0, e.g. -1, is to disable this feature - not recommended. +order_refresh_tolerance_pct: null + +# Size of your bid and ask order. +order_amount: null + +# How long to wait before placing the next order in case your order gets filled. +filled_order_delay: null + +# Target base asset inventory percentage target to be maintained (for Inventory skew feature). +inventory_target_base_pct: null + +# Whether to enable adding transaction costs to order price calculation (true/false). +add_transaction_costs: null + +# Avellaneda - Stoikov algorithm parameters +parameters_based_on_spread: null +min_spread: null +max_spread: null +vol_to_spread_multiplier: null +inventory_risk_aversion: null +order_book_depth_factor: null +risk_factor: null +order_amount_shape_factor: null +closing_time: null + +# Buffer size used to store historic samples and calculate volatility +volatility_buffer_size: 60 \ No newline at end of file