import time from datetime import datetime from decimal import Decimal from os.path import join, realpath from hummingbot.pmm_script.pmm_script_base import PMMScriptBase s_decimal_1 = Decimal("1") LOGS_PATH = realpath(join(__file__, "../../logs/")) SCRIPT_LOG_FILE = f"{LOGS_PATH}/logs_script.log" def log_to_file(file_name, message): with open(file_name, "a+") as f: f.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " - " + message + "\n") class SpreadsAdjustedOnVolatility(PMMScriptBase): """ Demonstrates how to adjust bid and ask spreads based on price volatility. The volatility, in this example, is simply a price change compared to the previous cycle regardless of its direction, e.g. if price changes -3% (or 3%), the volatility is 3%. To update our pure market making spreads, we're gonna smooth out the volatility by averaging it over a short period (short_period), and we need a benchmark to compare its value against. In this example the benchmark is a median long period price volatility (you can also use a fixed number, e.g. 3% - if you expect this to be the norm for your market). For example, if our bid_spread and ask_spread are at 0.8%, and the median long term volatility is 1.5%. Recently the volatility jumps to 2.6% (on short term average), we're gonna adjust both our bid and ask spreads to 1.9% (the original spread - 0.8% plus the volatility delta - 1.1%). Then after a short while the volatility drops back to 1.5%, our spreads are now adjusted back to 0.8%. """ # Let's set interval and sample sizes as below. # These numbers are for testing purposes only (in reality, they should be larger numbers) # interval is a interim which to pick historical mid price samples from, if you set it to 5, the first sample is # the last (current) mid price, the second sample is a past mid price 5 seconds before the last, and so on. interval = 5 # short_period is how many interval to pick the samples for the average short term volatility calculation, # for short_period of 3, this is 3 samples (5 seconds interval), of the last 15 seconds short_period = 3 # long_period is how many interval to pick the samples for the median long term volatility calculation, # for long_period of 10, this is 10 samples (5 seconds interval), of the last 50 seconds long_period = 10 last_stats_logged = 0 def __init__(self): super().__init__() self.original_bid_spread = None self.original_ask_spread = None self.avg_short_volatility = None self.median_long_volatility = None def volatility_msg(self, include_mid_price=False): if self.avg_short_volatility is None or self.median_long_volatility is None: return "short_volatility: N/A long_volatility: N/A" mid_price_msg = f" mid_price: {self.mid_price:<15}" if include_mid_price else "" return f"short_volatility: {self.avg_short_volatility:.2%} " \ f"long_volatility: {self.median_long_volatility:.2%}{mid_price_msg}" def on_tick(self): # First, let's keep the original spreads. if self.original_bid_spread is None: self.original_bid_spread = self.pmm_parameters.bid_spread self.original_ask_spread = self.pmm_parameters.ask_spread # Average volatility (price change) over a short period of time, this is to detect recent sudden changes. self.avg_short_volatility = self.avg_price_volatility(self.interval, self.short_period) # Median volatility over a long period of time, this is to find the market norm volatility. # We use median (instead of average) to find the middle volatility value - this is to avoid recent # spike affecting the average value. self.median_long_volatility = self.median_price_volatility(self.interval, self.long_period) # If the bot just got started, we'll not have these numbers yet as there is not enough mid_price sample size. # We'll start to have these numbers after interval * long_term_period. if self.avg_short_volatility is None or self.median_long_volatility is None: return # Let's log some stats once every 5 minutes if time.time() - self.last_stats_logged > 60 * 5: log_to_file(SCRIPT_LOG_FILE, self.volatility_msg(True)) self.last_stats_logged = time.time() # This volatility delta will be used to adjust spreads. delta = self.avg_short_volatility - self.median_long_volatility # Let's round the delta into 0.25% increment to ignore noise and to avoid adjusting the spreads too often. spread_adjustment = self.round_by_step(delta, Decimal("0.0025")) # Show the user on what's going, you can remove this statement to stop the notification. # self.notify(f"avg_short_volatility: {avg_short_volatility} median_long_volatility: {median_long_volatility} " # f"spread_adjustment: {spread_adjustment}") new_bid_spread = self.original_bid_spread + spread_adjustment # Let's not set the spreads below the originals, this is to avoid having spreads to be too close # to the mid price. new_bid_spread = max(self.original_bid_spread, new_bid_spread) old_bid_spread = self.pmm_parameters.bid_spread if new_bid_spread != self.pmm_parameters.bid_spread: self.pmm_parameters.bid_spread = new_bid_spread new_ask_spread = self.original_ask_spread + spread_adjustment new_ask_spread = max(self.original_ask_spread, new_ask_spread) if new_ask_spread != self.pmm_parameters.ask_spread: self.pmm_parameters.ask_spread = new_ask_spread if old_bid_spread != new_bid_spread: log_to_file(SCRIPT_LOG_FILE, self.volatility_msg(True)) log_to_file(SCRIPT_LOG_FILE, f"spreads adjustment: Old Value: {old_bid_spread:.2%} " f"New Value: {new_bid_spread:.2%}") def on_status(self) -> str: return self.volatility_msg()