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/command/create_command.py b/hummingbot/client/command/create_command.py index 4ca413be94..4aad717851 100644 --- a/hummingbot/client/command/create_command.py +++ b/hummingbot/client/command/create_command.py @@ -108,9 +108,11 @@ async def prompt_a_config(self, # type: HummingbotApplication if self.app.to_stop_config: return + config.value = parse_cvar_value(config, input_value) err_msg = await config.validate(input_value) if err_msg is not None: self._notify(err_msg) + config.value = None await self.prompt_a_config(config) else: config.value = parse_cvar_value(config, input_value) diff --git a/hummingbot/client/command/rate_command.py b/hummingbot/client/command/rate_command.py index a23135c0dd..b784e3c0c3 100644 --- a/hummingbot/client/command/rate_command.py +++ b/hummingbot/client/command/rate_command.py @@ -5,6 +5,7 @@ ) from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.client.errors import OracleRateUnavailable s_float_0 = float(0) s_decimal_0 = Decimal("0") @@ -29,14 +30,21 @@ def rate(self, # type: HummingbotApplication async def show_rate(self, # type: HummingbotApplication pair: str, ): + try: + msg = await RateCommand.oracle_rate_msg(pair) + except OracleRateUnavailable: + msg = "Rate is not available." + self._notify(msg) + + @staticmethod + async def oracle_rate_msg(pair: str, + ): pair = pair.upper() - self._notify(f"Source: {RateOracle.source.name}") rate = await RateOracle.rate_async(pair) if rate is None: - self._notify("Rate is not available.") - return + raise OracleRateUnavailable base, quote = pair.split("-") - self._notify(f"1 {base} = {rate} {quote}") + return f"Source: {RateOracle.source.name}\n1 {base} = {rate} {quote}" async def show_token_value(self, # type: HummingbotApplication token: str diff --git a/hummingbot/client/command/start_command.py b/hummingbot/client/command/start_command.py index 3c36475ee1..a8b516d9ec 100644 --- a/hummingbot/client/command/start_command.py +++ b/hummingbot/client/command/start_command.py @@ -17,17 +17,18 @@ from hummingbot.client.config.config_helpers import ( get_strategy_starter_file, ) -from hummingbot.client.settings import ( - STRATEGIES, - SCRIPTS_PATH, - required_exchanges, -) +import hummingbot.client.settings as settings from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.core.utils.kill_switch import KillSwitch from typing import TYPE_CHECKING from hummingbot.client.config.global_config_map import global_config_map from hummingbot.script.script_iterator import ScriptIterator from hummingbot.connector.connector_status import get_connector_status, warning_messages +from hummingbot.client.config.config_var import ConfigVar +from hummingbot.client.command.rate_command import RateCommand +from hummingbot.client.config.config_validators import validate_bool +from hummingbot.client.errors import OracleRateUnavailable +from hummingbot.core.rate_oracle.rate_oracle import RateOracle if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -57,15 +58,19 @@ def start(self, # type: HummingbotApplication async def start_check(self, # type: HummingbotApplication log_level: Optional[str] = None, restore: Optional[bool] = False): - if self.strategy_task is not None and not self.strategy_task.done(): self._notify('The bot is already running - please run "stop" first') return + if settings.required_rate_oracle: + if not (await self.confirm_oracle_conversion_rate()): + self._notify("The strategy failed to start.") + return + else: + RateOracle.get_instance().start() is_valid = await self.status_check_all(notify_success=False) if not is_valid: return - if self._last_started_strategy_file != self.strategy_file_name: init_logging("hummingbot_logs.yml", override_log_level=log_level.upper() if log_level else None, @@ -83,7 +88,7 @@ async def start_check(self, # type: HummingbotApplication if global_config_map.get("paper_trade_enabled").value: self._notify("\nPaper Trading ON: All orders are simulated, and no real orders are placed.") - for exchange in required_exchanges: + for exchange in settings.required_exchanges: connector = str(exchange) status = get_connector_status(connector) @@ -104,7 +109,7 @@ async def start_market_making(self, # type: HummingbotApplication strategy_name: str, restore: Optional[bool] = False): start_strategy: Callable = get_strategy_starter_file(strategy_name) - if strategy_name in STRATEGIES: + if strategy_name in settings.STRATEGIES: start_strategy(self) else: raise NotImplementedError @@ -131,7 +136,7 @@ async def start_market_making(self, # type: HummingbotApplication script_file = global_config_map["script_file_path"].value folder = dirname(script_file) if folder == "": - script_file = join(SCRIPTS_PATH, script_file) + script_file = join(settings.SCRIPTS_PATH, script_file) if self.strategy_name != "pure_market_making": self._notify("Error: script feature is only available for pure_market_making strategy (for now).") else: @@ -150,3 +155,30 @@ async def start_market_making(self, # type: HummingbotApplication await self.wait_till_ready(self.kill_switch.start) except Exception as e: self.logger().error(str(e), exc_info=True) + + async def confirm_oracle_conversion_rate(self, # type: HummingbotApplication + ) -> bool: + try: + result = False + self.app.clear_input() + self.placeholder_mode = True + self.app.hide_input = True + for pair in settings.rate_oracle_pairs: + msg = await RateCommand.oracle_rate_msg(pair) + self._notify("\nRate Oracle:\n" + msg) + config = ConfigVar(key="confirm_oracle_use", + type_str="bool", + prompt="Please confirm to proceed if the above oracle source and rates are correct for " + "this strategy (Yes/No) >>> ", + required_if=lambda: True, + validator=lambda v: validate_bool(v)) + await self.prompt_a_config(config) + if config.value: + result = True + except OracleRateUnavailable: + self._notify("Oracle rate is not available.") + finally: + self.placeholder_mode = False + self.app.hide_input = False + self.app.change_prompt(prompt=">>> ") + return result diff --git a/hummingbot/client/command/stop_command.py b/hummingbot/client/command/stop_command.py index 6eac19b03e..3413f2d90f 100644 --- a/hummingbot/client/command/stop_command.py +++ b/hummingbot/client/command/stop_command.py @@ -3,6 +3,7 @@ import threading from typing import TYPE_CHECKING from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.core.rate_oracle.rate_oracle import RateOracle if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -44,6 +45,9 @@ async def stop_loop(self, # type: HummingbotApplication if self.strategy_task is not None and not self.strategy_task.cancelled(): self.strategy_task.cancel() + if RateOracle.get_instance().started: + RateOracle.get_instance().stop() + if self.markets_recorder is not None: self.markets_recorder.stop() 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/client/config/global_config_map.py b/hummingbot/client/config/global_config_map.py index 57f37bbdb6..7a6c96fa23 100644 --- a/hummingbot/client/config/global_config_map.py +++ b/hummingbot/client/config/global_config_map.py @@ -343,11 +343,11 @@ def global_token_symbol_on_validated(value: str): default=RateOracleSource.binance.name), "global_token": ConfigVar(key="global_token", - prompt="What is your default display token? (e.g. USDT,USD,EUR) >>> ", + prompt="What is your default display token? (e.g. USD,EUR,BTC) >>> ", type_str="str", required_if=lambda: False, on_validated=global_token_on_validated, - default="USDT"), + default="USD"), "global_token_symbol": ConfigVar(key="global_token_symbol", prompt="What is your default display token symbol? (e.g. $,€) >>> ", diff --git a/hummingbot/client/errors.py b/hummingbot/client/errors.py index 54b19a407c..22e25d2d1f 100644 --- a/hummingbot/client/errors.py +++ b/hummingbot/client/errors.py @@ -7,3 +7,7 @@ class InvalidCommandError(Exception): class ArgumentParserError(Exception): pass + + +class OracleRateUnavailable(Exception): + pass diff --git a/hummingbot/client/settings.py b/hummingbot/client/settings.py index c832bcc7e4..19b12c058d 100644 --- a/hummingbot/client/settings.py +++ b/hummingbot/client/settings.py @@ -15,6 +15,9 @@ # Global variables required_exchanges: List[str] = [] requried_connector_trading_pairs: Dict[str, List[str]] = {} +# Set these two variables if a strategy uses oracle for rate conversion +required_rate_oracle: bool = False +rate_oracle_pairs: List[str] = [] # Global static values KEYFILE_PREFIX = "key_file_" diff --git a/hummingbot/client/ui/completer.py b/hummingbot/client/ui/completer.py index ba850a4bf1..bdb7ae66fa 100644 --- a/hummingbot/client/ui/completer.py +++ b/hummingbot/client/ui/completer.py @@ -68,11 +68,12 @@ def get_subcommand_completer(self, first_word: str) -> Completer: @property def _trading_pair_completer(self) -> Completer: trading_pair_fetcher = TradingPairFetcher.get_instance() + market = "" for exchange in sorted(list(CONNECTOR_SETTINGS.keys()), key=len, reverse=True): if exchange in self.prompt_text: market = exchange break - trading_pairs = trading_pair_fetcher.trading_pairs.get(market, []) if trading_pair_fetcher.ready else [] + trading_pairs = trading_pair_fetcher.trading_pairs.get(market, []) if trading_pair_fetcher.ready and market else [] return WordCompleter(trading_pairs, ignore_case=True, sentence=True) @property 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/core/rate_oracle/rate_oracle.py b/hummingbot/core/rate_oracle/rate_oracle.py index 14b7d31905..5fd847350c 100644 --- a/hummingbot/core/rate_oracle/rate_oracle.py +++ b/hummingbot/core/rate_oracle/rate_oracle.py @@ -19,20 +19,31 @@ class RateOracleSource(Enum): + """ + Supported sources for RateOracle + """ binance = 0 coingecko = 1 class RateOracle(NetworkBase): + """ + RateOracle provides conversion rates for any given pair token symbols in both async and sync fashions. + It achieves this by query URL on a given source for prices and store them, either in cache or as an object member. + The find_rate is then used on these prices to find a rate on a given pair. + """ + # Set these below class members before query for rates source: RateOracleSource = RateOracleSource.binance global_token: str = "USDT" global_token_symbol: str = "$" + _logger: Optional[HummingbotLogger] = None _shared_instance: "RateOracle" = None _shared_client: Optional[aiohttp.ClientSession] = None _cgecko_supported_vs_tokens: List[str] = [] binance_price_url = "https://api.binance.com/api/v3/ticker/bookTicker" + binance_us_price_url = "https://api.binance.us/api/v3/ticker/bookTicker" coingecko_usd_price_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency={}&order=market_cap_desc" \ "&per_page=250&page={}&sparkline=false" coingecko_supported_vs_tokens_url = "https://api.coingecko.com/api/v3/simple/supported_vs_currencies" @@ -64,6 +75,9 @@ async def _http_client(cls) -> aiohttp.ClientSession: return cls._shared_client async def get_ready(self): + """ + The network is ready when it first successfully get prices for a given source. + """ try: if not self._ready_event.is_set(): await self._ready_event.wait() @@ -79,27 +93,50 @@ def name(self) -> str: @property def prices(self) -> Dict[str, Decimal]: + """ + Actual prices retrieved from URL + """ return self._prices.copy() - def update_interval(self) -> float: - return 1.0 - def rate(self, pair: str) -> Decimal: + """ + Finds a conversion rate for a given symbol, this can be direct or indirect prices as long as it can find a route + to achieve this. + :param pair: A trading pair, e.g. BTC-USDT + :return A conversion rate + """ return find_rate(self._prices, pair) @classmethod async def rate_async(cls, pair: str) -> Decimal: + """ + Finds a conversion rate in an async operation, it is a class method which can be used directly without having to + start the RateOracle network. + :param pair: A trading pair, e.g. BTC-USDT + :return A conversion rate + """ prices = await cls.get_prices() return find_rate(prices, pair) @classmethod async def global_rate(cls, token: str) -> Decimal: + """ + Finds a conversion rate of a given token to a global token + :param token: A token symbol, e.g. BTC + :return A conversion rate + """ prices = await cls.get_prices() pair = token + "-" + cls.global_token return find_rate(prices, pair) @classmethod async def global_value(cls, token: str, amount: Decimal) -> Decimal: + """ + Finds a value of a given token amount in a global token unit + :param token: A token symbol, e.g. BTC + :param amount: An amount of token to be converted to value + :return A value of the token in global token unit + """ rate = await cls.global_rate(token) rate = Decimal("0") if rate is None else rate return amount * rate @@ -115,10 +152,14 @@ async def fetch_price_loop(self): except Exception: self.logger().network(f"Error fetching new prices from {self.source.name}.", exc_info=True, app_warning_msg=f"Couldn't fetch newest prices from {self.source.name}.") - await asyncio.sleep(self.update_interval()) + await asyncio.sleep(1) @classmethod async def get_prices(cls) -> Dict[str, Decimal]: + """ + Fetches prices of a specified source + :return A dictionary of trading pairs and prices + """ if cls.source == RateOracleSource.binance: return await cls.get_binance_prices() elif cls.source == RateOracleSource.coingecko: @@ -129,24 +170,57 @@ async def get_prices(cls) -> Dict[str, Decimal]: @classmethod @async_ttl_cache(ttl=1, maxsize=1) async def get_binance_prices(cls) -> Dict[str, Decimal]: + """ + Fetches Binance prices from binance.com and binance.us where only USD pairs from binance.us prices are added + to the prices dictionary. + :return A dictionary of trading pairs and prices + """ + results = {} + tasks = [cls.get_binance_prices_by_domain(cls.binance_price_url), + cls.get_binance_prices_by_domain(cls.binance_us_price_url, "USD")] + task_results = await safe_gather(*tasks, return_exceptions=True) + for task_result in task_results: + if isinstance(task_result, Exception): + cls.logger().error("Unexpected error while retrieving rates from Coingecko. " + "Check the log file for more info.") + break + else: + results.update(task_result) + return results + + @classmethod + async def get_binance_prices_by_domain(cls, url: str, quote_symbol: str = None) -> Dict[str, Decimal]: + """ + Fetches binance prices + :param url: A URL end point + :param quote_symbol: A quote symbol, if specified only pairs with the quote symbol are included for prices + :return A dictionary of trading pairs and prices + """ results = {} client = await cls._http_client() - try: - async with client.request("GET", cls.binance_price_url) as resp: - records = await resp.json() - for record in records: - trading_pair = binance_convert_from_exchange_pair(record["symbol"]) - if trading_pair and record["bidPrice"] is not None and record["askPrice"] is not None: - results[trading_pair] = (Decimal(record["bidPrice"]) + Decimal(record["askPrice"])) / Decimal("2") - except asyncio.CancelledError: - raise - except Exception: - cls.logger().error("Unexpected error while retrieving rates from Binance.") + async with client.request("GET", url) as resp: + records = await resp.json() + for record in records: + trading_pair = binance_convert_from_exchange_pair(record["symbol"]) + if quote_symbol is not None: + base, quote = trading_pair.split("-") + if quote != quote_symbol: + continue + if trading_pair and record["bidPrice"] is not None and record["askPrice"] is not None: + results[trading_pair] = (Decimal(record["bidPrice"]) + Decimal(record["askPrice"])) / Decimal( + "2") return results @classmethod @async_ttl_cache(ttl=30, maxsize=1) async def get_coingecko_prices(cls, vs_currency: str) -> Dict[str, Decimal]: + """ + Fetches CoinGecko prices for the top 1000 token (order by market cap), each API query returns 250 results, + hence it queries 4 times concurrently. + :param vs_currency: A currency (crypto or fiat) to get prices of tokens in, see + https://api.coingecko.com/api/v3/simple/supported_vs_currencies for the current supported list + :return A dictionary of trading pairs and prices + """ results = {} if not cls._cgecko_supported_vs_tokens: client = await cls._http_client() @@ -168,6 +242,13 @@ async def get_coingecko_prices(cls, vs_currency: str) -> Dict[str, Decimal]: @classmethod async def get_coingecko_prices_by_page(cls, vs_currency: str, page_no: int) -> Dict[str, Decimal]: + """ + Fetches CoinGecko prices by page number. + :param vs_currency: A currency (crypto or fiat) to get prices of tokens in, see + https://api.coingecko.com/api/v3/simple/supported_vs_currencies for the current supported list + :param page_no: The page number + :return A dictionary of trading pairs and prices (250 results max) + """ results = {} client = await cls._http_client() async with client.request("GET", cls.coingecko_usd_price_url.format(vs_currency, page_no)) as resp: diff --git a/hummingbot/core/rate_oracle/utils.py b/hummingbot/core/rate_oracle/utils.py index 0192705764..261c6dae79 100644 --- a/hummingbot/core/rate_oracle/utils.py +++ b/hummingbot/core/rate_oracle/utils.py @@ -16,6 +16,8 @@ def find_rate(prices: Dict[str, Decimal], pair: str) -> Decimal: if pair in prices: return prices[pair] base, quote = pair.split("-") + if base == quote: + return Decimal("1") reverse_pair = f"{quote}-{base}" if reverse_pair in prices: return Decimal("1") / prices[reverse_pair] 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/arbitrage/arbitrage.pxd b/hummingbot/strategy/arbitrage/arbitrage.pxd index 316fd6ede6..9d999eea9d 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pxd +++ b/hummingbot/strategy/arbitrage/arbitrage.pxd @@ -23,10 +23,12 @@ cdef class ArbitrageStrategy(StrategyBase): object _exchange_rate_conversion int _failed_order_tolerance bint _cool_off_logged + bint _use_oracle_conversion_rate object _secondary_to_primary_base_conversion_rate object _secondary_to_primary_quote_conversion_rate bint _hb_app_notification tuple _current_profitability + double _last_conv_rates_logged cdef tuple c_calculate_arbitrage_top_order_profitability(self, object market_pair) cdef c_process_market_pair(self, object market_pair) diff --git a/hummingbot/strategy/arbitrage/arbitrage.pyx b/hummingbot/strategy/arbitrage/arbitrage.pyx index d08a6afc83..b406d9a742 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pyx +++ b/hummingbot/strategy/arbitrage/arbitrage.pyx @@ -20,6 +20,8 @@ from hummingbot.core.network_iterator import NetworkStatus from hummingbot.strategy.strategy_base import StrategyBase from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple from hummingbot.strategy.arbitrage.arbitrage_market_pair import ArbitrageMarketPair +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.client.performance import smart_round NaN = float("nan") s_decimal_0 = Decimal(0) @@ -49,6 +51,7 @@ cdef class ArbitrageStrategy(StrategyBase): status_report_interval: float = 60.0, next_trade_delay_interval: float = 15.0, failed_order_tolerance: int = 1, + use_oracle_conversion_rate: bool = False, secondary_to_primary_base_conversion_rate: Decimal = Decimal("1"), secondary_to_primary_quote_conversion_rate: Decimal = Decimal("1"), hb_app_notification: bool = False): @@ -75,9 +78,10 @@ cdef class ArbitrageStrategy(StrategyBase): self._failed_order_tolerance = failed_order_tolerance self._cool_off_logged = False self._current_profitability = () - + self._use_oracle_conversion_rate = use_oracle_conversion_rate self._secondary_to_primary_base_conversion_rate = secondary_to_primary_base_conversion_rate self._secondary_to_primary_quote_conversion_rate = secondary_to_primary_quote_conversion_rate + self._last_conv_rates_logged = 0 self._hb_app_notification = hb_app_notification @@ -106,6 +110,55 @@ cdef class ArbitrageStrategy(StrategyBase): def tracked_market_orders_data_frame(self) -> List[pd.DataFrame]: return self._sb_order_tracker.tracked_market_orders_data_frame + def get_second_to_first_conversion_rate(self) -> Tuple[str, Decimal, str, Decimal]: + """ + Find conversion rates from secondary market to primary market + :return: A tuple of quote pair symbol, quote conversion rate source, quote conversion rate, + base pair symbol, base conversion rate source, base conversion rate + """ + quote_rate = Decimal("1") + quote_pair = f"{self._market_pairs[0].second.quote_asset}-{self._market_pairs[0].first.quote_asset}" + quote_rate_source = "fixed" + if self._use_oracle_conversion_rate: + if self._market_pairs[0].second.quote_asset != self._market_pairs[0].first.quote_asset: + quote_rate_source = RateOracle.source.name + quote_rate = RateOracle.get_instance().rate(quote_pair) + else: + quote_rate = self._secondary_to_primary_quote_conversion_rate + base_rate = Decimal("1") + base_pair = f"{self._market_pairs[0].second.base_asset}-{self._market_pairs[0].first.base_asset}" + base_rate_source = "fixed" + if self._use_oracle_conversion_rate: + if self._market_pairs[0].second.base_asset != self._market_pairs[0].first.base_asset: + base_rate_source = RateOracle.source.name + base_rate = RateOracle.get_instance().rate(base_pair) + else: + base_rate = self._secondary_to_primary_base_conversion_rate + return quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate + + def log_conversion_rates(self): + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate = \ + self.get_second_to_first_conversion_rate() + if quote_pair.split("-")[0] != quote_pair.split("-")[1]: + self.logger().info(f"{quote_pair} ({quote_rate_source}) conversion rate: {smart_round(quote_rate)}") + if base_pair.split("-")[0] != base_pair.split("-")[1]: + self.logger().info(f"{base_pair} ({base_rate_source}) conversion rate: {smart_round(base_rate)}") + + def oracle_status_df(self): + columns = ["Source", "Pair", "Rate"] + data = [] + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate = \ + self.get_second_to_first_conversion_rate() + if quote_pair.split("-")[0] != quote_pair.split("-")[1]: + data.extend([ + [quote_rate_source, quote_pair, smart_round(quote_rate)], + ]) + if base_pair.split("-")[0] != base_pair.split("-")[1]: + data.extend([ + [base_rate_source, base_pair, smart_round(base_rate)], + ]) + return pd.DataFrame(data=data, columns=columns) + def format_status(self) -> str: cdef: list lines = [] @@ -117,6 +170,11 @@ cdef class ArbitrageStrategy(StrategyBase): lines.extend(["", " Markets:"] + [" " + line for line in str(markets_df).split("\n")]) + oracle_df = self.oracle_status_df() + if not oracle_df.empty: + lines.extend(["", " Rate conversion:"] + + [" " + line for line in str(oracle_df).split("\n")]) + assets_df = self.wallet_balance_data_frame([market_pair.first, market_pair.second]) lines.extend(["", " Assets:"] + [" " + line for line in str(assets_df).split("\n")]) @@ -194,6 +252,10 @@ cdef class ArbitrageStrategy(StrategyBase): for market_pair in self._market_pairs: self.c_process_market_pair(market_pair) + # log conversion rates every 5 minutes + if self._last_conv_rates_logged + (60. * 5) < self._current_timestamp: + self.log_conversion_rates() + self._last_conv_rates_logged = self._current_timestamp finally: self._last_timestamp = timestamp @@ -390,7 +452,20 @@ cdef class ArbitrageStrategy(StrategyBase): if market_info == self._market_pairs[0].first: return Decimal("1") elif market_info == self._market_pairs[0].second: - return self._secondary_to_primary_quote_conversion_rate / self._secondary_to_primary_base_conversion_rate + _, _, quote_rate, _, _, base_rate = self.get_second_to_first_conversion_rate() + return quote_rate / base_rate + # if not self._use_oracle_conversion_rate: + # return self._secondary_to_primary_quote_conversion_rate / self._secondary_to_primary_base_conversion_rate + # else: + # quote_rate = Decimal("1") + # if self._market_pairs[0].second.quote_asset != self._market_pairs[0].first.quote_asset: + # quote_pair = f"{self._market_pairs[0].second.quote_asset}-{self._market_pairs[0].first.quote_asset}" + # quote_rate = RateOracle.get_instance().rate(quote_pair) + # base_rate = Decimal("1") + # if self._market_pairs[0].second.base_asset != self._market_pairs[0].first.base_asset: + # base_pair = f"{self._market_pairs[0].second.base_asset}-{self._market_pairs[0].first.base_asset}" + # base_rate = RateOracle.get_instance().rate(base_pair) + # return quote_rate / base_rate cdef tuple c_find_best_profitable_amount(self, object buy_market_trading_pair_tuple, object sell_market_trading_pair_tuple): """ diff --git a/hummingbot/strategy/arbitrage/arbitrage_config_map.py b/hummingbot/strategy/arbitrage/arbitrage_config_map.py index 96313098ae..781396f642 100644 --- a/hummingbot/strategy/arbitrage/arbitrage_config_map.py +++ b/hummingbot/strategy/arbitrage/arbitrage_config_map.py @@ -2,12 +2,11 @@ from hummingbot.client.config.config_validators import ( validate_exchange, validate_market_trading_pair, - validate_decimal -) -from hummingbot.client.settings import ( - required_exchanges, - EXAMPLE_PAIRS, + validate_decimal, + validate_bool ) +from hummingbot.client.config.config_helpers import parse_cvar_value +import hummingbot.client.settings as settings from decimal import Decimal from typing import Optional @@ -24,70 +23,109 @@ def validate_secondary_market_trading_pair(value: str) -> Optional[str]: def primary_trading_pair_prompt(): primary_market = arbitrage_config_map.get("primary_market").value - example = EXAMPLE_PAIRS.get(primary_market) + example = settings.EXAMPLE_PAIRS.get(primary_market) return "Enter the token trading pair you would like to trade on %s%s >>> " \ % (primary_market, f" (e.g. {example})" if example else "") def secondary_trading_pair_prompt(): secondary_market = arbitrage_config_map.get("secondary_market").value - example = EXAMPLE_PAIRS.get(secondary_market) + example = settings.EXAMPLE_PAIRS.get(secondary_market) return "Enter the token trading pair you would like to trade on %s%s >>> " \ % (secondary_market, f" (e.g. {example})" if example else "") def secondary_market_on_validated(value: str): - required_exchanges.append(value) + settings.required_exchanges.append(value) + + +def update_oracle_settings(value: str): + c_map = arbitrage_config_map + if not (c_map["use_oracle_conversion_rate"].value is not None and + c_map["primary_market_trading_pair"].value is not None and + c_map["secondary_market_trading_pair"].value is not None): + return + use_oracle = parse_cvar_value(c_map["use_oracle_conversion_rate"], c_map["use_oracle_conversion_rate"].value) + first_base, first_quote = c_map["primary_market_trading_pair"].value.split("-") + second_base, second_quote = c_map["secondary_market_trading_pair"].value.split("-") + if use_oracle and (first_base != second_base or first_quote != second_quote): + settings.required_rate_oracle = True + settings.rate_oracle_pairs = [] + if first_base != second_base: + settings.rate_oracle_pairs.append(f"{second_base}-{first_base}") + if first_quote != second_quote: + settings.rate_oracle_pairs.append(f"{second_quote}-{first_quote}") + else: + settings.required_rate_oracle = False + settings.rate_oracle_pairs = [] arbitrage_config_map = { - "strategy": - ConfigVar(key="strategy", - prompt="", - default="arbitrage"), + "strategy": ConfigVar( + key="strategy", + prompt="", + default="arbitrage" + ), "primary_market": ConfigVar( key="primary_market", prompt="Enter your primary spot connector >>> ", prompt_on_new=True, validator=validate_exchange, - on_validated=lambda value: required_exchanges.append(value)), + on_validated=lambda value: settings.required_exchanges.append(value), + ), "secondary_market": ConfigVar( key="secondary_market", prompt="Enter your secondary spot connector >>> ", prompt_on_new=True, validator=validate_exchange, - on_validated=secondary_market_on_validated), + on_validated=secondary_market_on_validated, + ), "primary_market_trading_pair": ConfigVar( key="primary_market_trading_pair", prompt=primary_trading_pair_prompt, prompt_on_new=True, - validator=validate_primary_market_trading_pair), + validator=validate_primary_market_trading_pair, + on_validated=update_oracle_settings, + ), "secondary_market_trading_pair": ConfigVar( key="secondary_market_trading_pair", prompt=secondary_trading_pair_prompt, prompt_on_new=True, - validator=validate_secondary_market_trading_pair), + validator=validate_secondary_market_trading_pair, + on_validated=update_oracle_settings, + ), "min_profitability": ConfigVar( key="min_profitability", prompt="What is the minimum profitability for you to make a trade? (Enter 1 to indicate 1%) >>> ", prompt_on_new=True, default=Decimal("0.3"), validator=lambda v: validate_decimal(v, Decimal(-100), Decimal("100"), inclusive=True), - type_str="decimal"), + type_str="decimal", + ), + "use_oracle_conversion_rate": ConfigVar( + key="use_oracle_conversion_rate", + type_str="bool", + prompt="Do you want to use rate oracle on unmatched trading pairs? (Yes/No) >>> ", + prompt_on_new=True, + validator=lambda v: validate_bool(v), + on_validated=update_oracle_settings, + ), "secondary_to_primary_base_conversion_rate": ConfigVar( key="secondary_to_primary_base_conversion_rate", prompt="Enter conversion rate for secondary base asset value to primary base asset value, e.g. " - "if primary base asset is USD, secondary is DAI and 1 USD is worth 1.25 DAI, " - "the conversion rate is 0.8 (1 / 1.25) >>> ", + "if primary base asset is USD and the secondary is DAI, 1 DAI is valued at 1.25 USD, " + "the conversion rate is 1.25 >>> ", default=Decimal("1"), - validator=lambda v: validate_decimal(v, Decimal(0), Decimal("100"), inclusive=False), - type_str="decimal"), + validator=lambda v: validate_decimal(v, Decimal(0), inclusive=False), + type_str="decimal", + ), "secondary_to_primary_quote_conversion_rate": ConfigVar( key="secondary_to_primary_quote_conversion_rate", prompt="Enter conversion rate for secondary quote asset value to primary quote asset value, e.g. " - "if primary quote asset is USD, secondary is DAI and 1 USD is worth 1.25 DAI, " - "the conversion rate is 0.8 (1 / 1.25) >>> ", + "if primary quote asset is USD and the secondary is DAI and 1 DAI is valued at 1.25 USD, " + "the conversion rate is 1.25 >>> ", default=Decimal("1"), - validator=lambda v: validate_decimal(v, Decimal(0), Decimal("100"), inclusive=False), - type_str="decimal"), + validator=lambda v: validate_decimal(v, Decimal(0), inclusive=False), + type_str="decimal", + ), } diff --git a/hummingbot/strategy/arbitrage/start.py b/hummingbot/strategy/arbitrage/start.py index e9afc4f3ba..b429e8cb89 100644 --- a/hummingbot/strategy/arbitrage/start.py +++ b/hummingbot/strategy/arbitrage/start.py @@ -15,6 +15,7 @@ def start(self): raw_primary_trading_pair = arbitrage_config_map.get("primary_market_trading_pair").value raw_secondary_trading_pair = arbitrage_config_map.get("secondary_market_trading_pair").value min_profitability = arbitrage_config_map.get("min_profitability").value / Decimal("100") + use_oracle_conversion_rate = arbitrage_config_map.get("use_oracle_conversion_rate").value secondary_to_primary_base_conversion_rate = arbitrage_config_map["secondary_to_primary_base_conversion_rate"].value secondary_to_primary_quote_conversion_rate = arbitrage_config_map["secondary_to_primary_quote_conversion_rate"].value @@ -41,6 +42,7 @@ def start(self): self.strategy = ArbitrageStrategy(market_pairs=[self.market_pair], min_profitability=min_profitability, logging_options=ArbitrageStrategy.OPTION_LOG_ALL, + use_oracle_conversion_rate=use_oracle_conversion_rate, secondary_to_primary_base_conversion_rate=secondary_to_primary_base_conversion_rate, secondary_to_primary_quote_conversion_rate=secondary_to_primary_quote_conversion_rate, hb_app_notification=True) 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/strategy/cross_exchange_market_making/cross_exchange_market_making.pxd b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pxd index af6556e069..ac99e62386 100755 --- a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pxd +++ b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pxd @@ -31,10 +31,12 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): dict _market_pairs int64_t _logging_options OrderIDMarketPairTracker _market_pair_tracker + bint _use_oracle_conversion_rate object _taker_to_maker_base_conversion_rate object _taker_to_maker_quote_conversion_rate bint _hb_app_notification list _maker_order_ids + double _last_conv_rates_logged cdef c_process_market_pair(self, object market_pair, diff --git a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pyx b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pyx index 6bc8d1e66c..7f56c692ec 100755 --- a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pyx +++ b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pyx @@ -9,6 +9,7 @@ from math import ( ceil ) from numpy import isnan +import pandas as pd from typing import ( List, Tuple, @@ -28,6 +29,8 @@ from hummingbot.strategy.strategy_base cimport StrategyBase from hummingbot.strategy.strategy_base import StrategyBase from .cross_exchange_market_pair import CrossExchangeMarketPair from .order_id_market_pair_tracker import OrderIDMarketPairTracker +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.client.performance import smart_round NaN = float("nan") s_decimal_zero = Decimal(0) @@ -73,6 +76,7 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): top_depth_tolerance: Decimal = Decimal(0), logging_options: int = OPTION_LOG_ALL, status_report_interval: float = 900, + use_oracle_conversion_rate: bool = False, taker_to_maker_base_conversion_rate: Decimal = Decimal("1"), taker_to_maker_quote_conversion_rate: Decimal = Decimal("1"), hb_app_notification: bool = False @@ -132,8 +136,10 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): self._status_report_interval = status_report_interval self._market_pair_tracker = OrderIDMarketPairTracker() self._adjust_orders_enabled = adjust_order_enabled + self._use_oracle_conversion_rate = use_oracle_conversion_rate self._taker_to_maker_base_conversion_rate = taker_to_maker_base_conversion_rate self._taker_to_maker_quote_conversion_rate = taker_to_maker_quote_conversion_rate + self._last_conv_rates_logged = 0 self._hb_app_notification = hb_app_notification self._maker_order_ids = [] @@ -167,6 +173,56 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): def logging_options(self, int64_t logging_options): self._logging_options = logging_options + def get_taker_to_maker_conversion_rate(self) -> Tuple[str, Decimal, str, Decimal]: + """ + Find conversion rates from taker market to maker market + :return: A tuple of quote pair symbol, quote conversion rate source, quote conversion rate, + base pair symbol, base conversion rate source, base conversion rate + """ + quote_rate = Decimal("1") + market_pairs = list(self._market_pairs.values())[0] + quote_pair = f"{market_pairs.taker.quote_asset}-{market_pairs.maker.quote_asset}" + quote_rate_source = "fixed" + if self._use_oracle_conversion_rate: + if market_pairs.taker.quote_asset != market_pairs.maker.quote_asset: + quote_rate_source = RateOracle.source.name + quote_rate = RateOracle.get_instance().rate(quote_pair) + else: + quote_rate = self._taker_to_maker_quote_conversion_rate + base_rate = Decimal("1") + base_pair = f"{market_pairs.taker.base_asset}-{market_pairs.maker.base_asset}" + base_rate_source = "fixed" + if self._use_oracle_conversion_rate: + if market_pairs.taker.base_asset != market_pairs.maker.base_asset: + base_rate_source = RateOracle.source.name + base_rate = RateOracle.get_instance().rate(base_pair) + else: + base_rate = self._taker_to_maker_base_conversion_rate + return quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate + + def log_conversion_rates(self): + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate = \ + self.get_taker_to_maker_conversion_rate() + if quote_pair.split("-")[0] != quote_pair.split("-")[1]: + self.logger().info(f"{quote_pair} ({quote_rate_source}) conversion rate: {smart_round(quote_rate)}") + if base_pair.split("-")[0] != base_pair.split("-")[1]: + self.logger().info(f"{base_pair} ({base_rate_source}) conversion rate: {smart_round(base_rate)}") + + def oracle_status_df(self): + columns = ["Source", "Pair", "Rate"] + data = [] + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate = \ + self.get_taker_to_maker_conversion_rate() + if quote_pair.split("-")[0] != quote_pair.split("-")[1]: + data.extend([ + [quote_rate_source, quote_pair, smart_round(quote_rate)], + ]) + if base_pair.split("-")[0] != base_pair.split("-")[1]: + data.extend([ + [base_rate_source, base_pair, smart_round(base_rate)], + ]) + return pd.DataFrame(data=data, columns=columns) + def format_status(self) -> str: cdef: list lines = [] @@ -190,6 +246,11 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): lines.extend(["", " Markets:"] + [" " + line for line in str(markets_df).split("\n")]) + oracle_df = self.oracle_status_df() + if not oracle_df.empty: + lines.extend(["", " Rate conversion:"] + + [" " + line for line in str(oracle_df).split("\n")]) + assets_df = self.wallet_balance_data_frame([market_pair.maker, market_pair.taker]) lines.extend(["", " Assets:"] + [" " + line for line in str(assets_df).split("\n")]) @@ -305,6 +366,10 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): # Process each market pair independently. for market_pair in self._market_pairs.values(): self.c_process_market_pair(market_pair, market_pair_to_active_orders[market_pair]) + # log conversion rates every 5 minutes + if self._last_conv_rates_logged + (60. * 5) < self._current_timestamp: + self.log_conversion_rates() + self._last_conv_rates_logged = self._current_timestamp finally: self._last_timestamp = timestamp @@ -1070,13 +1135,16 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): object order_price = active_order.price ExchangeBase maker_market = market_pair.maker.market ExchangeBase taker_market = market_pair.taker.market - - object quote_asset_amount = maker_market.c_get_balance(market_pair.maker.quote_asset) if is_buy else \ - taker_market.c_get_balance(market_pair.taker.quote_asset) - object base_asset_amount = taker_market.c_get_balance(market_pair.taker.base_asset) if is_buy else \ - maker_market.c_get_balance(market_pair.maker.base_asset) object order_size_limit + quote_pair, quote_rate_source, quote_rate, base_pair, base_rate_source, base_rate = \ + self.get_taker_to_maker_conversion_rate() + + quote_asset_amount = maker_market.c_get_balance(market_pair.maker.quote_asset) if is_buy else \ + taker_market.c_get_balance(market_pair.taker.quote_asset) * quote_rate + base_asset_amount = taker_market.c_get_balance(market_pair.taker.base_asset) * base_rate if is_buy else \ + maker_market.c_get_balance(market_pair.maker.base_asset) + order_size_limit = min(base_asset_amount, quote_asset_amount / order_price) quantized_size_limit = maker_market.c_quantize_order_amount(active_order.trading_pair, order_size_limit) @@ -1096,7 +1164,15 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): """ Return price conversion rate for a taker market (to convert it into maker base asset value) """ - return self._taker_to_maker_quote_conversion_rate / self._taker_to_maker_base_conversion_rate + _, _, quote_rate, _, _, base_rate = self.get_taker_to_maker_conversion_rate() + return quote_rate / base_rate + # else: + # market_pairs = list(self._market_pairs.values())[0] + # quote_pair = f"{market_pairs.taker.quote_asset}-{market_pairs.maker.quote_asset}" + # base_pair = f"{market_pairs.taker.base_asset}-{market_pairs.maker.base_asset}" + # quote_rate = RateOracle.get_instance().rate(quote_pair) + # base_rate = RateOracle.get_instance().rate(base_pair) + # return quote_rate / base_rate cdef c_check_and_create_new_orders(self, object market_pair, bint has_active_bid, bint has_active_ask): """ @@ -1126,15 +1202,15 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): True, bid_size ) - effective_hedging_price_adjusted = effective_hedging_price * self.market_conversion_rate() + effective_hedging_price_adjusted = effective_hedging_price / self.market_conversion_rate() if self._logging_options & self.OPTION_LOG_CREATE_ORDER: self.log_with_clock( logging.INFO, f"({market_pair.maker.trading_pair}) Creating limit bid order for " f"{bid_size} {market_pair.maker.base_asset} at " f"{bid_price} {market_pair.maker.quote_asset}. " - f"Current hedging price: {effective_hedging_price} {market_pair.taker.quote_asset} " - f"(Rate adjusted: {effective_hedging_price_adjusted:.2f} {market_pair.taker.quote_asset})." + f"Current hedging price: {effective_hedging_price:.8f} {market_pair.maker.quote_asset} " + f"(Rate adjusted: {effective_hedging_price_adjusted:.8f} {market_pair.taker.quote_asset})." ) order_id = self.c_place_order(market_pair, True, True, bid_size, bid_price) else: @@ -1165,15 +1241,15 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): False, ask_size ) - effective_hedging_price_adjusted = effective_hedging_price + effective_hedging_price_adjusted = effective_hedging_price / self.market_conversion_rate() if self._logging_options & self.OPTION_LOG_CREATE_ORDER: self.log_with_clock( logging.INFO, f"({market_pair.maker.trading_pair}) Creating limit ask order for " f"{ask_size} {market_pair.maker.base_asset} at " f"{ask_price} {market_pair.maker.quote_asset}. " - f"Current hedging price: {effective_hedging_price} {market_pair.maker.quote_asset} " - f"(Rate adjusted: {effective_hedging_price_adjusted:.2f} {market_pair.maker.quote_asset})." + f"Current hedging price: {effective_hedging_price:.8f} {market_pair.maker.quote_asset} " + f"(Rate adjusted: {effective_hedging_price_adjusted:.8f} {market_pair.taker.quote_asset})." ) order_id = self.c_place_order(market_pair, False, True, ask_size, ask_price) else: diff --git a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making_config_map.py b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making_config_map.py index 65eaaa0554..b621b0b554 100644 --- a/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making_config_map.py +++ b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making_config_map.py @@ -5,7 +5,8 @@ validate_decimal, validate_bool ) -from hummingbot.client.settings import required_exchanges, EXAMPLE_PAIRS +from hummingbot.client.config.config_helpers import parse_cvar_value +import hummingbot.client.settings as settings from decimal import Decimal from hummingbot.client.config.config_helpers import ( minimum_order_amount @@ -15,7 +16,7 @@ def maker_trading_pair_prompt(): maker_market = cross_exchange_market_making_config_map.get("maker_market").value - example = EXAMPLE_PAIRS.get(maker_market) + example = settings.EXAMPLE_PAIRS.get(maker_market) return "Enter the token trading pair you would like to trade on maker market: %s%s >>> " % ( maker_market, f" (e.g. {example})" if example else "", @@ -24,7 +25,7 @@ def maker_trading_pair_prompt(): def taker_trading_pair_prompt(): taker_market = cross_exchange_market_making_config_map.get("taker_market").value - example = EXAMPLE_PAIRS.get(taker_market) + example = settings.EXAMPLE_PAIRS.get(taker_market) return "Enter the token trading pair you would like to trade on taker market: %s%s >>> " % ( taker_market, f" (e.g. {example})" if example else "", @@ -68,7 +69,28 @@ async def validate_order_amount(value: str) -> Optional[str]: def taker_market_on_validated(value: str): - required_exchanges.append(value) + settings.required_exchanges.append(value) + + +def update_oracle_settings(value: str): + c_map = cross_exchange_market_making_config_map + if not (c_map["use_oracle_conversion_rate"].value is not None and + c_map["maker_market_trading_pair"].value is not None and + c_map["taker_market_trading_pair"].value is not None): + return + use_oracle = parse_cvar_value(c_map["use_oracle_conversion_rate"], c_map["use_oracle_conversion_rate"].value) + first_base, first_quote = c_map["maker_market_trading_pair"].value.split("-") + second_base, second_quote = c_map["taker_market_trading_pair"].value.split("-") + if use_oracle and (first_base != second_base or first_quote != second_quote): + settings.required_rate_oracle = True + settings.rate_oracle_pairs = [] + if first_base != second_base: + settings.rate_oracle_pairs.append(f"{second_base}-{first_base}") + if first_quote != second_quote: + settings.rate_oracle_pairs.append(f"{second_quote}-{first_quote}") + else: + settings.required_rate_oracle = False + settings.rate_oracle_pairs = [] cross_exchange_market_making_config_map = { @@ -81,7 +103,7 @@ def taker_market_on_validated(value: str): prompt="Enter your maker spot connector >>> ", prompt_on_new=True, validator=validate_exchange, - on_validated=lambda value: required_exchanges.append(value), + on_validated=lambda value: settings.required_exchanges.append(value), ), "taker_market": ConfigVar( key="taker_market", @@ -94,13 +116,15 @@ def taker_market_on_validated(value: str): key="maker_market_trading_pair", prompt=maker_trading_pair_prompt, prompt_on_new=True, - validator=validate_maker_market_trading_pair + validator=validate_maker_market_trading_pair, + on_validated=update_oracle_settings ), "taker_market_trading_pair": ConfigVar( key="taker_market_trading_pair", prompt=taker_trading_pair_prompt, prompt_on_new=True, - validator=validate_taker_market_trading_pair + validator=validate_taker_market_trading_pair, + on_validated=update_oracle_settings ), "min_profitability": ConfigVar( key="min_profitability", @@ -193,22 +217,29 @@ def taker_market_on_validated(value: str): required_if=lambda: False, validator=lambda v: validate_decimal(v, Decimal(0), Decimal(100), inclusive=False) ), + "use_oracle_conversion_rate": ConfigVar( + key="use_oracle_conversion_rate", + type_str="bool", + prompt="Do you want to use rate oracle on unmatched trading pairs? (Yes/No) >>> ", + prompt_on_new=True, + validator=lambda v: validate_bool(v), + on_validated=update_oracle_settings), "taker_to_maker_base_conversion_rate": ConfigVar( key="taker_to_maker_base_conversion_rate", prompt="Enter conversion rate for taker base asset value to maker base asset value, e.g. " - "if maker base asset is USD, taker is DAI and 1 USD is worth 1.25 DAI, " - "the conversion rate is 0.8 (1 / 1.25) >>> ", + "if maker base asset is USD and the taker is DAI, 1 DAI is valued at 1.25 USD, " + "the conversion rate is 1.25 >>> ", default=Decimal("1"), - validator=lambda v: validate_decimal(v, Decimal(0), Decimal("100"), inclusive=False), + validator=lambda v: validate_decimal(v, Decimal(0), inclusive=False), type_str="decimal" ), "taker_to_maker_quote_conversion_rate": ConfigVar( key="taker_to_maker_quote_conversion_rate", prompt="Enter conversion rate for taker quote asset value to maker quote asset value, e.g. " - "if taker quote asset is USD, maker is DAI and 1 USD is worth 1.25 DAI, " - "the conversion rate is 0.8 (1 / 1.25) >>> ", + "if maker quote asset is USD and the taker is DAI, 1 DAI is valued at 1.25 USD, " + "the conversion rate is 1.25 >>> ", default=Decimal("1"), - validator=lambda v: validate_decimal(v, Decimal(0), Decimal("100"), inclusive=False), + validator=lambda v: validate_decimal(v, Decimal(0), inclusive=False), type_str="decimal" ), } diff --git a/hummingbot/strategy/cross_exchange_market_making/start.py b/hummingbot/strategy/cross_exchange_market_making/start.py index c3ffe6ef98..a735d69863 100644 --- a/hummingbot/strategy/cross_exchange_market_making/start.py +++ b/hummingbot/strategy/cross_exchange_market_making/start.py @@ -28,6 +28,7 @@ def start(self): order_size_taker_balance_factor = xemm_map.get("order_size_taker_balance_factor").value / Decimal("100") order_size_portfolio_ratio_limit = xemm_map.get("order_size_portfolio_ratio_limit").value / Decimal("100") anti_hysteresis_duration = xemm_map.get("anti_hysteresis_duration").value + use_oracle_conversion_rate = xemm_map.get("use_oracle_conversion_rate").value taker_to_maker_base_conversion_rate = xemm_map.get("taker_to_maker_base_conversion_rate").value taker_to_maker_quote_conversion_rate = xemm_map.get("taker_to_maker_quote_conversion_rate").value @@ -83,6 +84,7 @@ def start(self): order_size_taker_balance_factor=order_size_taker_balance_factor, order_size_portfolio_ratio_limit=order_size_portfolio_ratio_limit, anti_hysteresis_duration=anti_hysteresis_duration, + use_oracle_conversion_rate=use_oracle_conversion_rate, taker_to_maker_base_conversion_rate=taker_to_maker_base_conversion_rate, taker_to_maker_quote_conversion_rate=taker_to_maker_quote_conversion_rate, hb_app_notification=True, diff --git a/hummingbot/templates/conf_arbitrage_strategy_TEMPLATE.yml b/hummingbot/templates/conf_arbitrage_strategy_TEMPLATE.yml index c383731e90..7459d5f5b6 100644 --- a/hummingbot/templates/conf_arbitrage_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_arbitrage_strategy_TEMPLATE.yml @@ -2,7 +2,7 @@ ### Arbitrage strategy config ### ##################################### -template_version: 4 +template_version: 5 strategy: null # The following configuations are only required for the @@ -18,6 +18,10 @@ secondary_market_trading_pair: null # Expressed in percentage value, e.g. 1 = 1% target profit min_profitability: null +# Whether to use rate oracle on unmatched trading pairs +# Set this to either True or False +use_oracle_conversion_rate: null + # The conversion rate for secondary base asset value to primary base asset value. # e.g. if primary base asset is USD, secondary is DAI and 1 USD is worth 1.25 DAI, " # the conversion rate is 0.8 (1 / 1.25) 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 diff --git a/hummingbot/templates/conf_cross_exchange_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_cross_exchange_market_making_strategy_TEMPLATE.yml index dff79727cf..f435dbdbaa 100644 --- a/hummingbot/templates/conf_cross_exchange_market_making_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_cross_exchange_market_making_strategy_TEMPLATE.yml @@ -2,7 +2,7 @@ ### Cross exchange market making strategy config ### ######################################################## -template_version: 4 +template_version: 5 strategy: null # The following configuations are only required for the @@ -60,6 +60,10 @@ order_size_taker_balance_factor: null # in terms of ratio of total portfolio value on both maker and taker markets order_size_portfolio_ratio_limit: null +# Whether to use rate oracle on unmatched trading pairs +# Set this to either True or False +use_oracle_conversion_rate: null + # The conversion rate for taker base asset value to maker base asset value. # e.g. if maker base asset is USD, taker is DAI and 1 USD is worth 1.25 DAI, " # the conversion rate is 0.8 (1 / 1.25) diff --git a/hummingbot/templates/conf_global_TEMPLATE.yml b/hummingbot/templates/conf_global_TEMPLATE.yml index 508af5d1ff..84946fb27f 100644 --- a/hummingbot/templates/conf_global_TEMPLATE.yml +++ b/hummingbot/templates/conf_global_TEMPLATE.yml @@ -189,8 +189,11 @@ heartbeat_interval_min: # a list of binance markets (for trades/pnl reporting) separated by ',' e.g. RLC-USDT,RLC-BTC binance_markets: +# A source for rate oracle, currently binance or coingecko rate_oracle_source: +# A universal token which to display tokens values in, e.g. USD,EUR,BTC global_token: +# A symbol for the global token, e.g. $, € global_token_symbol: \ No newline at end of file diff --git a/setup.py b/setup.py index 8a0c12f2d2..fc70445f2d 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ # for C/ObjC but not for C++ class BuildExt(build_ext): def build_extensions(self): - if '-Wstrict-prototypes' in self.compiler.compiler_so: + if os.name != "nt" and '-Wstrict-prototypes' in self.compiler.compiler_so: self.compiler.compiler_so.remove('-Wstrict-prototypes') super().build_extensions() diff --git a/test/test_rate_oracle.py b/test/test_rate_oracle.py index 9ecefae688..cbe1fe8d32 100644 --- a/test/test_rate_oracle.py +++ b/test/test_rate_oracle.py @@ -59,3 +59,20 @@ def test_find_rate(self): self.assertEqual(rate, Decimal("0.5")) rate = find_rate(prices, "HBOT-GBP") self.assertEqual(rate, Decimal("75")) + + def test_get_binance_prices(self): + asyncio.get_event_loop().run_until_complete(self._test_get_binance_prices()) + + async def _test_get_binance_prices(self): + com_prices = await RateOracle.get_binance_prices_by_domain(RateOracle.binance_price_url) + print(com_prices) + self.assertGreater(len(com_prices), 1) + us_prices = await RateOracle.get_binance_prices_by_domain(RateOracle.binance_us_price_url, "USD") + print(us_prices) + self.assertGreater(len(us_prices), 1) + quotes = {p.split("-")[1] for p in us_prices} + self.assertEqual(len(quotes), 1) + self.assertEqual(list(quotes)[0], "USD") + combined_prices = await RateOracle.get_binance_prices() + self.assertGreater(len(combined_prices), 1) + self.assertGreater(len(combined_prices), len(com_prices))