From c878d859403ded8f64391db1ee30f60b1af9a19d Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 15 Feb 2021 12:10:19 -0300 Subject: [PATCH 01/77] first commit --- .../pure_market_making_as/__init__.py | 12 + .../api_asset_price_delegate.pxd | 4 + .../api_asset_price_delegate.pyx | 19 + .../asset_price_delegate.pxd | 3 + .../asset_price_delegate.pyx | 16 + .../pure_market_making_as/data_types.py | 55 + .../order_book_asset_price_delegate.pxd | 7 + .../order_book_asset_price_delegate.pyx | 29 + .../pure_market_making_as.pxd | 93 ++ .../pure_market_making_as.pyx | 1199 +++++++++++++++++ .../pure_market_making_as_config_map.py | 221 +++ .../pure_market_making_as_order_tracker.pxd | 8 + .../pure_market_making_as_order_tracker.pyx | 49 + .../strategy/pure_market_making_as/start.py | 77 ++ hummingbot/strategy/utils/__init__.py | 0 hummingbot/strategy/utils/ring_buffer.pxd | 21 + hummingbot/strategy/utils/ring_buffer.pyx | 67 + ...ure_market_making_as_strategy_TEMPLATE.yml | 63 + 18 files changed, 1943 insertions(+) create mode 100644 hummingbot/strategy/pure_market_making_as/__init__.py create mode 100644 hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd create mode 100644 hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx create mode 100644 hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd create mode 100644 hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx create mode 100644 hummingbot/strategy/pure_market_making_as/data_types.py create mode 100644 hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd create mode 100644 hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx create mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd create mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx create mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py create mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd create mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx create mode 100644 hummingbot/strategy/pure_market_making_as/start.py create mode 100644 hummingbot/strategy/utils/__init__.py create mode 100644 hummingbot/strategy/utils/ring_buffer.pxd create mode 100644 hummingbot/strategy/utils/ring_buffer.pyx create mode 100644 hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml diff --git a/hummingbot/strategy/pure_market_making_as/__init__.py b/hummingbot/strategy/pure_market_making_as/__init__.py new file mode 100644 index 0000000000..598802efe2 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from .pure_market_making_as import PureMarketMakingASStrategy +from .asset_price_delegate import AssetPriceDelegate +from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate +from .api_asset_price_delegate import APIAssetPriceDelegate +__all__ = [ + PureMarketMakingASStrategy, + AssetPriceDelegate, + OrderBookAssetPriceDelegate, + APIAssetPriceDelegate, +] diff --git a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd new file mode 100644 index 0000000000..c37fb04d40 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd @@ -0,0 +1,4 @@ +from .asset_price_delegate cimport AssetPriceDelegate + +cdef class APIAssetPriceDelegate(AssetPriceDelegate): + cdef object _custom_api_feed diff --git a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx new file mode 100644 index 0000000000..5134db639e --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx @@ -0,0 +1,19 @@ +from .asset_price_delegate cimport AssetPriceDelegate +from hummingbot.data_feed.custom_api_data_feed import CustomAPIDataFeed, NetworkStatus + +cdef class APIAssetPriceDelegate(AssetPriceDelegate): + def __init__(self, api_url: str): + super().__init__() + self._custom_api_feed = CustomAPIDataFeed(api_url=api_url) + self._custom_api_feed.start() + + cdef object c_get_mid_price(self): + return self._custom_api_feed.get_price() + + @property + def ready(self) -> bool: + return self._custom_api_feed.network_status == NetworkStatus.CONNECTED + + @property + def custom_api_feed(self) -> CustomAPIDataFeed: + return self._custom_api_feed diff --git a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd new file mode 100644 index 0000000000..af6a7bf0fd --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd @@ -0,0 +1,3 @@ + +cdef class AssetPriceDelegate: + cdef object c_get_mid_price(self) diff --git a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx new file mode 100644 index 0000000000..c68f3d665f --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx @@ -0,0 +1,16 @@ +from decimal import Decimal + + +cdef class AssetPriceDelegate: + # The following exposed Python functions are meant for unit tests + # --------------------------------------------------------------- + def get_mid_price(self) -> Decimal: + return self.c_get_mid_price() + # --------------------------------------------------------------- + + cdef object c_get_mid_price(self): + raise NotImplementedError + + @property + def ready(self) -> bool: + raise NotImplementedError diff --git a/hummingbot/strategy/pure_market_making_as/data_types.py b/hummingbot/strategy/pure_market_making_as/data_types.py new file mode 100644 index 0000000000..4a8c1f5d04 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/data_types.py @@ -0,0 +1,55 @@ +#!/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 InventorySkewBidAskRatios(NamedTuple): + bid_ratio: float + ask_ratio: float + + +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/pure_market_making_as/order_book_asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd new file mode 100644 index 0000000000..e787cf878c --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd @@ -0,0 +1,7 @@ +from .asset_price_delegate cimport AssetPriceDelegate +from hummingbot.connector.exchange_base cimport ExchangeBase + +cdef class OrderBookAssetPriceDelegate(AssetPriceDelegate): + cdef: + ExchangeBase _market + str _trading_pair diff --git a/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx new file mode 100644 index 0000000000..0383401698 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx @@ -0,0 +1,29 @@ +from hummingbot.core.event.events import PriceType +from .asset_price_delegate cimport AssetPriceDelegate +from hummingbot.connector.exchange_base import ExchangeBase +from decimal import Decimal + +cdef class OrderBookAssetPriceDelegate(AssetPriceDelegate): + def __init__(self, market: ExchangeBase, trading_pair: str): + super().__init__() + self._market = market + self._trading_pair = trading_pair + + cdef object c_get_mid_price(self): + return (self._market.c_get_price(self._trading_pair, True) + + self._market.c_get_price(self._trading_pair, False))/Decimal('2') + + @property + def ready(self) -> bool: + return self._market.ready + + def get_price_by_type(self, price_type: PriceType) -> Decimal: + return self._market.get_price_by_type(self._trading_pair, price_type) + + @property + def market(self) -> ExchangeBase: + return self._market + + @property + def trading_pair(self) -> str: + return self._trading_pair diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd new file mode 100644 index 0000000000..39b9e8992a --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -0,0 +1,93 @@ +# distutils: language=c++ + +from libc.stdint cimport int64_t +from hummingbot.strategy.strategy_base cimport StrategyBase +from ..utils.ring_buffer cimport RingBuffer + + +cdef class PureMarketMakingASStrategy(StrategyBase): + cdef: + object _market_info + + object _bid_spread + object _ask_spread + object _minimum_spread + object _order_amount + int _order_levels + int _buy_levels + int _sell_levels + object _order_level_spread + object _order_level_amount + double _order_refresh_time + double _max_order_age + object _order_refresh_tolerance_pct + double _filled_order_delay + bint _inventory_skew_enabled + object _inventory_target_base_pct + object _inventory_range_multiplier + bint _hanging_orders_enabled + object _hanging_orders_cancel_pct + bint _order_optimization_enabled + object _ask_order_optimization_depth + object _bid_order_optimization_depth + bint _add_transaction_costs_to_orders + object _asset_price_delegate + object _inventory_cost_price_delegate + object _price_type + bint _take_if_crossed + object _price_ceiling + object _price_floor + bint _ping_pong_enabled + list _ping_pong_warning_lines + bint _hb_app_notification + object _order_override + + double _cancel_timestamp + double _create_timestamp + object _limit_order_type + bint _all_markets_ready + int _filled_buys_balance + int _filled_sells_balance + list _hanging_order_ids + double _last_timestamp + double _status_report_interval + int64_t _logging_options + object _last_own_trade_price + list _hanging_aged_order_prices + double _kappa + double _gamma + double _closing_time + double _time_left + double _reserved_price + double _optimal_spread + double _optimal_bid + double _optimal_ask + RingBuffer _mid_prices + RingBuffer _spreads + + 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_levels_modifiers(self, object proposal) + cdef c_apply_price_band(self, object proposal) + cdef c_apply_ping_pong(self, object proposal) + cdef c_apply_order_price_modifiers(self, object proposal) + cdef c_apply_budget_constraint(self, object proposal) + + cdef c_filter_out_takers(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_cancel_hanging_orders(self) + 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 c_save_mid_price(self) + cdef double c_get_spread(self) + cdef c_save_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) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx new file mode 100644 index 0000000000..cc99bed70a --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -0,0 +1,1199 @@ +from decimal import Decimal +import logging +import os.path +import pandas as pd +import numpy as np +from typing import ( + List, + Dict, + Optional +) +from math import ( + floor, + ceil +) +import time +from hummingbot.core.clock cimport Clock +from hummingbot.core.event.events import TradeType, PriceType +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 .pure_market_making_as_order_tracker import PureMarketMakingASOrderTracker + +from .asset_price_delegate cimport AssetPriceDelegate +from .asset_price_delegate import AssetPriceDelegate +from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate +from ..utils.ring_buffer cimport RingBuffer + + +NaN = float("nan") +s_decimal_zero = Decimal(0) +s_decimal_neg_one = Decimal(-1) +pmm_logger = None + + +cdef class PureMarketMakingASStrategy(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, + inventory_target_base_pct: Decimal = s_decimal_zero, + add_transaction_costs_to_orders: bool = False, + asset_price_delegate: AssetPriceDelegate = None, + price_type: str = "mid_price", + take_if_crossed: bool = False, + price_ceiling: Decimal = s_decimal_neg_one, + price_floor: Decimal = s_decimal_neg_one, + ping_pong_enabled: bool = False, + logging_options: int = OPTION_LOG_ALL, + status_report_interval: float = 900, + hb_app_notification: bool = False, + order_override: Dict[str, List[str]] = {}, + kappa: float = 0.1, + gamma: float = 0.5, + closing_time: float = 3600.0 * 24 * 1e3, + ): + super().__init__() + self._sb_order_tracker = PureMarketMakingASOrderTracker() + self._market_info = market_info + self._order_amount = order_amount + self._order_refresh_time = order_refresh_time + self._max_order_age = max_order_age + self._inventory_target_base_pct = inventory_target_base_pct + self._add_transaction_costs_to_orders = add_transaction_costs_to_orders + self._asset_price_delegate = asset_price_delegate + self._price_type = self.get_price_type(price_type) + self._take_if_crossed = take_if_crossed + self._price_ceiling = price_ceiling + self._price_floor = price_floor + self._ping_pong_enabled = ping_pong_enabled + self._ping_pong_warning_lines = [] + self._hb_app_notification = hb_app_notification + self._order_override = order_override + + self._cancel_timestamp = 0 + self._create_timestamp = 0 + self._hanging_aged_order_prices = [] + self._limit_order_type = self._market_info.market.get_maker_order_type() + if take_if_crossed: + self._limit_order_type = OrderType.LIMIT + self._all_markets_ready = False + self._filled_buys_balance = 0 + self._filled_sells_balance = 0 + self._hanging_order_ids = [] + 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._mid_prices = RingBuffer(5) + self._spreads = RingBuffer(5) + self._kappa = kappa + self._gamma = gamma + self._time_left = closing_time + self._closing_time = closing_time + self._reserved_price = 0 + self._optimal_spread = 0 + self._optimal_ask = 0 + self._optimal_bid = 0 + + 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 order_levels(self) -> int: + return self._order_levels + + @order_levels.setter + def order_levels(self, value: int): + self._order_levels = value + self._buy_levels = value + self._sell_levels = value + + @property + def buy_levels(self) -> int: + return self._buy_levels + + @buy_levels.setter + def buy_levels(self, value: int): + self._buy_levels = value + + @property + def sell_levels(self) -> int: + return self._sell_levels + + @sell_levels.setter + def sell_levels(self, value: int): + self._sell_levels = value + + @property + def order_level_amount(self) -> Decimal: + return self._order_level_amount + + @order_level_amount.setter + def order_level_amount(self, value: Decimal): + self._order_level_amount = value + + @property + def order_level_spread(self) -> Decimal: + return self._order_level_spread + + @order_level_spread.setter + def order_level_spread(self, value: Decimal): + self._order_level_spread = value + + @property + def inventory_skew_enabled(self) -> bool: + return self._inventory_skew_enabled + + @inventory_skew_enabled.setter + def inventory_skew_enabled(self, value: bool): + self._inventory_skew_enabled = 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 inventory_range_multiplier(self) -> Decimal: + return self._inventory_range_multiplier + + @inventory_range_multiplier.setter + def inventory_range_multiplier(self, value: Decimal): + self._inventory_range_multiplier = value + + @property + def hanging_orders_enabled(self) -> bool: + return self._hanging_orders_enabled + + @hanging_orders_enabled.setter + def hanging_orders_enabled(self, value: bool): + self._hanging_orders_enabled = value + + @property + def hanging_orders_cancel_pct(self) -> Decimal: + return self._hanging_orders_cancel_pct + + @hanging_orders_cancel_pct.setter + def hanging_orders_cancel_pct(self, value: Decimal): + self._hanging_orders_cancel_pct = value + + @property + def bid_spread(self) -> Decimal: + return self._bid_spread + + @bid_spread.setter + def bid_spread(self, value: Decimal): + self._bid_spread = value + + @property + def ask_spread(self) -> Decimal: + return self._ask_spread + + @ask_spread.setter + def ask_spread(self, value: Decimal): + self._ask_spread = 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 price_ceiling(self) -> Decimal: + return self._price_ceiling + + @price_ceiling.setter + def price_ceiling(self, value: Decimal): + self._price_ceiling = value + + @property + def price_floor(self) -> Decimal: + return self._price_floor + + @price_floor.setter + def price_floor(self, value: Decimal): + self._price_floor = 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 + + @property + def order_override(self): + return self._order_override + + @order_override.setter + def order_override(self, value: Dict[str, List[str]]): + self._order_override = value + + def get_price(self) -> float: + price_provider = self._asset_price_delegate or self._market_info + if self._price_type is PriceType.LastOwnTrade: + price = self._last_own_trade_price + elif self._price_type is PriceType.InventoryCost: + price = price_provider.get_price_by_type(PriceType.MidPrice) + else: + price = price_provider.get_price_by_type(self._price_type) + + if price.is_nan(): + price = price_provider.get_price_by_type(PriceType.MidPrice) + + return 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): + cdef: + AssetPriceDelegate delegate = self._asset_price_delegate + object mid_price + if self._asset_price_delegate is not None: + mid_price = delegate.c_get_mid_price() + else: + mid_price = self._market_info.get_mid_price() + return mid_price + + @property + def hanging_order_ids(self) -> List[str]: + return self._hanging_order_ids + + @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 active_non_hanging_orders(self) -> List[LimitOrder]: + orders = [o for o in self.active_orders if o.client_order_id not in self._hanging_order_ids] + return orders + + @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 asset_price_delegate(self) -> AssetPriceDelegate: + return self._asset_price_delegate + + @asset_price_delegate.setter + def asset_price_delegate(self, value): + self._asset_price_delegate = value + + @property + def inventory_cost_price_delegate(self) -> AssetPriceDelegate: + return self._inventory_cost_price_delegate + + @inventory_cost_price_delegate.setter + def inventory_cost_price_delegate(self, value): + self._inventory_cost_price_delegate = value + + @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: + 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 not in self._hanging_order_ids]) + 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] + level = None + if order.client_order_id not in self._hanging_order_ids: + if order.is_buy: + level = lvl_buy + 1 + lvl_buy += 1 + else: + level = no_sells - lvl_sell + lvl_sell += 1 + 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 = "" if level is None else self._order_amount + ((level - 1) * self._order_level_amount) + data.append([ + "hang" if order.client_order_id in self._hanging_order_ids else level, + "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"Ref Price ({self._price_type.name})"] + if self._price_type is PriceType.LastOwnTrade and self._last_own_trade_price.is_nan(): + markets_columns[-1] = "Ref Price (MidPrice)" + market_books = [(self._market_info.market, self._market_info.trading_pair)] + if type(self._asset_price_delegate) is OrderBookAssetPriceDelegate: + market_books.append((self._asset_price_delegate.market, self._asset_price_delegate.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 = float("nan") + if market == self._market_info.market and self._inventory_cost_price_delegate is not None: + # We're using inventory_cost, show it's price + ref_price = self._inventory_cost_price_delegate.get_price() + if ref_price is None: + ref_price = self.get_price() + elif market == self._market_info.market and self._asset_price_delegate is None: + ref_price = self.get_price() + elif ( + self._asset_price_delegate is not None + and market == self._asset_price_delegate.market + and self._price_type is not PriceType.LastOwnTrade + ): + ref_price = self._asset_price_delegate.get_price_by_type(self._price_type) + markets_data.append([ + market.display_name, + trading_pair, + float(bid_price), + float(ask_price), + float(ref_price) + ]) + 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._ping_pong_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(not self._inventory_skew_enabled) + # append inventory skew stats. + if self._inventory_skew_enabled: + inventory_skew_df = self.inventory_skew_stats_data_frame() + assets_df = assets_df.append(inventory_skew_df) + + 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're 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."]) + + 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) + # make restored order hanging orders + for order_id in restored_order_ids: + self._hanging_order_ids.append(order_id) + + 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([market.ready for market in self._sb_markets]) + if self._asset_price_delegate is not None and self._all_markets_ready: + self._all_markets_ready = self._asset_price_delegate.ready + 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([market.network_status is NetworkStatus.CONNECTED for market 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.") + + csv_filename = "PMM_AS.csv" + csv_path = '/Users/nicolas/Desktop/'+csv_filename + self.c_collect_market_variables(timestamp) + if self.c_is_algorithm_ready(): + self.c_calculate_reserved_price_and_optimal_spread() + if not os.path.exists(csv_path): + df_header = pd.DataFrame([('mid_price', + 'spread', + 'reserved_price', + 'optimal_spread', + 'q', + 'time_left_fraction', + 'std_dev', + 'gamma', + 'kappa')]) + df_header.to_csv(csv_path, mode='a', header=False, index=False) + df = pd.DataFrame([(self._mid_prices.c_get_last_value(), + self._spreads.c_get_last_value(), + self._reserved_price, + self._optimal_spread, + self.c_calculate_target_inventory(), + self._time_left/self._closing_time, + self._mid_prices.c_std_dev(), + self._gamma, + self._kappa)]) + df.to_csv(csv_path, mode='a', header=False, index=False) + + proposal = None + asset_mid_price = Decimal("0") + # asset_mid_price = self.c_set_mid_price(market_info) + if self._create_timestamp <= self._current_timestamp: + # 1. Create base order proposals + proposal = self.c_create_base_proposal() + # 2. Apply functions that limit numbers of buys and sells proposal + self.c_apply_order_levels_modifiers(proposal) + # 3. Apply functions that modify orders price + self.c_apply_order_price_modifiers(proposal) + # 5. Apply budget constraint, i.e. can't buy/sell more than what you have. + self.c_apply_budget_constraint(proposal) + + if not self._take_if_crossed: + self.c_filter_out_takers(proposal) + self.c_cancel_active_orders(proposal) + self.c_cancel_hanging_orders() + 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) + finally: + self._last_timestamp = timestamp + + cdef c_collect_market_variables(self, double timestamp): + self.c_save_mid_price() + self.c_save_spread() + self._time_left = max(self._closing_time - (timestamp-self._last_timestamp), 0) + + cdef c_save_mid_price(self): + self._mid_prices.c_add_value(self.c_get_mid_price()) + + cdef c_save_spread(self): + self._spreads.c_add_value(self.c_get_spread()) + + 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)) + + cdef c_calculate_reserved_price_and_optimal_spread(self): + cdef: + ExchangeBase market = self._market_info.market + double mid_price + double base_balance + double mid_price_variance + double time_left_fraction = self._time_left / self._closing_time + double buy_fee + + if self.c_is_algorithm_ready(): + mid_price = self._mid_prices.c_get_last_value() + q = float(self.c_calculate_target_inventory()) + mid_price_variance = self._mid_prices.c_variance() + self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + np.log(1 + self._gamma / self._kappa) + self._optimal_ask = self._reserved_price + self._optimal_spread / 2 + self._optimal_bid = self._reserved_price + self._optimal_spread / 2 + + 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 + double mid_price + double base_value + double inventory_value + double target_inventory_value + double q + + mid_price = self._mid_prices.c_get_last_value() + # Need to review this to see if adjusted quantities are required + base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) + base_value = float(base_asset_amount) * mid_price + inventory_value = base_value + float(quote_asset_amount) + target_inventory_value = inventory_value * float(self._inventory_target_base_pct) + q = market.c_quantize_order_amount(trading_pair, target_inventory_value / mid_price) + + self.logger().info(f"mid:{mid_price} | base_amt:{float(base_asset_amount)} | base_value:{base_value} | inv_value:{inventory_value} | q_value: {target_inventory_value} | q:{q}") + + return q + + cdef bint c_is_algorithm_ready(self): + return self._mid_prices.c_is_full() + + cdef object c_create_base_proposal(self): + cdef: + ExchangeBase market = self._market_info.market + list buys = [] + list sells = [] + + base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) + delta_quantity = float(base_asset_amount) - self.c_calculate_target_inventory() + + self.logger().info(f"delta_quantity:{delta_quantity}") + + if delta_quantity > 0: + price = self._reserved_price - self._optimal_spread / 2 + price = market.c_quantize_order_price(self.trading_pair, price) + size = market.c_quantize_order_amount(self.trading_pair, delta_quantity) + if size > 0: + buys.append(PriceSize(price, size)) + + if delta_quantity < 0: + price = self._reserved_price + self._optimal_spread / 2 + price = market.c_quantize_order_price(self.trading_pair, price) + size = market.c_quantize_order_amount(self.trading_pair, delta_quantity) + 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_levels_modifiers(self, proposal): + self.c_apply_price_band(proposal) + if self._ping_pong_enabled: + self.c_apply_ping_pong(proposal) + + cdef c_apply_price_band(self, proposal): + if self._price_ceiling > 0 and self.get_price() >= self._price_ceiling: + proposal.buys = [] + if self._price_floor > 0 and self.get_price() <= self._price_floor: + proposal.sells = [] + + cdef c_apply_ping_pong(self, object proposal): + self._ping_pong_warning_lines = [] + if self._filled_buys_balance == self._filled_sells_balance: + self._filled_buys_balance = self._filled_sells_balance = 0 + if self._filled_buys_balance > 0: + proposal.buys = proposal.buys[self._filled_buys_balance:] + self._ping_pong_warning_lines.extend( + [f" Ping-pong removed {self._filled_buys_balance} buy orders."] + ) + if self._filled_sells_balance > 0: + proposal.sells = proposal.sells[self._filled_sells_balance:] + self._ping_pong_warning_lines.extend( + [f" Ping-pong removed {self._filled_sells_balance} sell orders."] + ) + + 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_non_hanging_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) + # self.logger().info(f"Not enough balance for buy order (Size: {buy.size.normalize()}, Price: {buy.price.normalize()}), " + # f"order_amount is adjusted to {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) + # self.logger().info(f"Not enough balance for sell order (Size: {sell.size.normalize()}, Price: {sell.price.normalize()}), " + # f"order_amount is adjusted to {adjusted_amount}") + 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] + + cdef c_filter_out_takers(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + list new_buys = [] + list new_sells = [] + top_ask = market.c_get_price(self.trading_pair, True) + if not top_ask.is_nan(): + proposal.buys = [buy for buy in proposal.buys if buy.price < top_ask] + top_bid = market.c_get_price(self.trading_pair, False) + if not top_bid.is_nan(): + proposal.sells = [sell for sell in proposal.sells if sell.price > top_bid] + + # 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 + + 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, self._bid_order_optimization_depth + 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 order_level_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) * (1 - self.order_level_spread * i) + + 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, self._ask_order_optimization_depth + 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 order_level_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) * (1 + self.order_level_spread * i) + + 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." + ) + + if self._inventory_cost_price_delegate is not None: + self._inventory_cost_price_delegate.process_order_fill_event(order_filled_event) + + 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] + + if self._hanging_orders_enabled: + # If the filled order is a hanging order, do nothing + if order_id in self._hanging_order_ids: + self.log_with_clock( + logging.INFO, + f"({self.trading_pair}) Hanging 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"Hanging maker BUY order {limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." + ) + return + + # delay order creation by filled_order_dalay (in seconds) + self._create_timestamp = self._current_timestamp + self._filled_order_delay + self._cancel_timestamp = min(self._cancel_timestamp, self._create_timestamp) + + if self._hanging_orders_enabled: + for other_order_id in active_sell_ids: + self._hanging_order_ids.append(other_order_id) + + 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] + if self._hanging_orders_enabled: + # If the filled order is a hanging order, do nothing + if order_id in self._hanging_order_ids: + self.log_with_clock( + logging.INFO, + f"({self.trading_pair}) Hanging 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"Hanging maker SELL order {limit_order_record.quantity} {limit_order_record.base_currency} @ " + f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." + ) + return + + # delay order creation by filled_order_dalay (in seconds) + self._create_timestamp = self._current_timestamp + self._filled_order_delay + self._cancel_timestamp = min(self._cancel_timestamp, self._create_timestamp) + + if self._hanging_orders_enabled: + for other_order_id in active_buy_ids: + self._hanging_order_ids.append(other_order_id) + + 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): + return False + # 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 non hanging 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_non_hanging_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.logger().info(f"Not cancelling active orders since difference between new order prices " + # f"and current order prices is within " + # f"{self._order_refresh_tolerance_pct:.2%} order_refresh_tolerance_pct") + self.set_timers() + + cdef c_cancel_hanging_orders(self): + 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: + object price = self.get_price() + list active_orders = self.active_orders + list orders + LimitOrder order + for h_order_id in self._hanging_order_ids: + orders = [o for o in active_orders if o.client_order_id == h_order_id] + if orders and price > 0: + order = orders[0] + if abs(order.price - price)/price >= self._hanging_orders_cancel_pct: + self.c_cancel_order(self._market_info, order.client_order_id) + + # 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)) + if order.client_order_id in self._hanging_order_ids: + self._hanging_aged_order_prices.append(order.price) + 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 and \ + len(self.active_non_hanging_orders) == 0 + + 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 + ) + if buy.price in self._hanging_aged_order_prices: + self._hanging_order_ids.append(bid_order_id) + self._hanging_aged_order_prices.remove(buy.price) + 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 + ) + if sell.price in self._hanging_aged_order_prices: + self._hanging_order_ids.append(ask_order_id) + self._hanging_aged_order_prices.remove(sell.price) + 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 get_price_type(self, price_type_str: str) -> PriceType: + if price_type_str == "mid_price": + return PriceType.MidPrice + elif price_type_str == "best_bid": + return PriceType.BestBid + elif price_type_str == "best_ask": + return PriceType.BestAsk + elif price_type_str == "last_price": + return PriceType.LastTrade + elif price_type_str == 'last_own_trade_price': + return PriceType.LastOwnTrade + elif price_type_str == 'inventory_cost': + return PriceType.InventoryCost + else: + raise ValueError(f"Unrecognized price type string {price_type_str}.") diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py new file mode 100644 index 0000000000..d9d8d7ff3f --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -0,0 +1,221 @@ +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.config_helpers import ( + minimum_order_amount, +) +from typing import Optional + + +def maker_trading_pair_prompt(): + exchange = pure_market_making_as_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 = pure_market_making_as_config_map.get("exchange").value + return validate_market_trading_pair(exchange, value) + + +def order_amount_prompt() -> str: + exchange = pure_market_making_as_config_map["exchange"].value + trading_pair = pure_market_making_as_config_map["market"].value + base_asset, quote_asset = trading_pair.split("-") + min_amount = minimum_order_amount(exchange, trading_pair) + return f"What is the amount of {base_asset} per order? (minimum {min_amount}) >>> " + + +def validate_order_amount(value: str) -> Optional[str]: + try: + exchange = pure_market_making_as_config_map["exchange"].value + trading_pair = pure_market_making_as_config_map["market"].value + min_amount = 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 validate_price_source(value: str) -> Optional[str]: + if value not in {"current_market", "external_market", "custom_api"}: + return "Invalid price source type." + + +def on_validate_price_source(value: str): + if value != "external_market": + pure_market_making_as_config_map["price_source_exchange"].value = None + pure_market_making_as_config_map["price_source_market"].value = None + pure_market_making_as_config_map["take_if_crossed"].value = None + if value != "custom_api": + pure_market_making_as_config_map["price_source_custom_api"].value = None + else: + pure_market_making_as_config_map["price_type"].value = None + + +def price_source_market_prompt() -> str: + external_market = pure_market_making_as_config_map.get("price_source_exchange").value + return f'Enter the token trading pair on {external_market} >>> ' + + +def validate_price_source_exchange(value: str) -> Optional[str]: + if value == pure_market_making_as_config_map.get("exchange").value: + return "Price source exchange cannot be the same as maker exchange." + return validate_exchange(value) + + +def on_validated_price_source_exchange(value: str): + if value is None: + pure_market_making_as_config_map["price_source_market"].value = None + + +def validate_price_source_market(value: str) -> Optional[str]: + market = pure_market_making_as_config_map.get("price_source_exchange").value + return validate_market_trading_pair(market, value) + + +def validate_price_floor_ceiling(value: str) -> Optional[str]: + try: + decimal_value = Decimal(value) + except Exception: + return f"{value} is not in decimal format." + if not (decimal_value == Decimal("-1") or decimal_value > Decimal("0")): + return "Value must be more than 0 or -1 to disable this feature." + + +def exchange_on_validated(value: str): + required_exchanges.append(value) + + +pure_market_making_as_config_map = { + "strategy": + ConfigVar(key="strategy", + prompt=None, + default="pure_market_making_as"), + "exchange": + ConfigVar(key="exchange", + prompt="Enter your maker exchange name >>> ", + 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), + "kappa": + ConfigVar(key="kappa", + prompt="Enter order book depth variable (kappa) >>> ", + type_str="float", + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True), + "gamma": + ConfigVar(key="gamma", + prompt="Enter risk factor (gamma) >>> ", + type_str="float", + validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), + prompt_on_new=True), + "closing_time": + ConfigVar(key="closing_time", + prompt="Enter closing time in days >>> ", + type_str="float", + validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), + prompt_on_new=True), + "order_refresh_time": + ConfigVar(key="order_refresh_time", + prompt="How often do you want to cancel and replace bids and asks " + "(in seconds)? >>> ", + type_str="float", + validator=lambda v: validate_decimal(v, 0, inclusive=False), + 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), + "ping_pong_enabled": + ConfigVar(key="ping_pong_enabled", + prompt="Would you like to use the ping pong feature and alternate between buy and sell orders after fills? (Yes/No) >>> ", + type_str="bool", + default=False, + prompt_on_new=True, + validator=validate_bool), + "inventory_target_base_pct": + ConfigVar(key="inventory_target_base_pct", + prompt="What is your target base asset percentage? Enter 50 for 50% >>> ", + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100), + 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), + "price_source": + ConfigVar(key="price_source", + prompt="Which price source to use? (current_market/external_market/custom_api) >>> ", + type_str="str", + default="current_market", + validator=validate_price_source, + on_validated=on_validate_price_source), + "price_type": + ConfigVar(key="price_type", + prompt="Which price type to use? (" + "mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost) >>> ", + type_str="str", + required_if=lambda: pure_market_making_as_config_map.get("price_source").value != "custom_api", + default="mid_price", + validator=lambda s: None if s in {"mid_price", + "last_price", + "last_own_trade_price", + "best_bid", + "best_ask", + "inventory_cost", + } else + "Invalid price type."), + "price_source_exchange": + ConfigVar(key="price_source_exchange", + prompt="Enter external price source exchange name >>> ", + required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "external_market", + type_str="str", + validator=validate_price_source_exchange, + on_validated=on_validated_price_source_exchange), + "price_source_market": + ConfigVar(key="price_source_market", + prompt=price_source_market_prompt, + required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "external_market", + type_str="str", + validator=validate_price_source_market), + "take_if_crossed": + ConfigVar(key="take_if_crossed", + prompt="Do you want to take the best order if orders cross the orderbook? ((Yes/No) >>> ", + required_if=lambda: pure_market_making_as_config_map.get( + "price_source").value == "external_market", + type_str="bool", + validator=validate_bool), + "price_source_custom_api": + ConfigVar(key="price_source_custom_api", + prompt="Enter pricing API URL >>> ", + required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "custom_api", + type_str="str"), + "order_override": + ConfigVar(key="order_override", + prompt=None, + required_if=lambda: False, + default=None, + type_str="json"), +} diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd new file mode 100644 index 0000000000..e25c74d225 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd @@ -0,0 +1,8 @@ +# distutils: language=c++ + +from hummingbot.strategy.order_tracker import OrderTracker +from hummingbot.strategy.order_tracker cimport OrderTracker + + +cdef class PureMarketMakingOrderTracker(OrderTracker): + pass diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx new file mode 100644 index 0000000000..1296b24b04 --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx @@ -0,0 +1,49 @@ +from typing import ( + Dict, + List, + Tuple +) + +from hummingbot.core.data_type.limit_order cimport LimitOrder +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.order_tracker cimport OrderTracker + +NaN = float("nan") + + +cdef class PureMarketMakingASOrderTracker(OrderTracker): + # ETH confirmation requirement of Binance has shortened to 12 blocks as of 7/15/2019. + # 12 * 15 / 60 = 3 minutes + SHADOW_MAKER_ORDER_KEEP_ALIVE_DURATION = 60.0 * 3 + + def __init__(self): + super().__init__() + + @property + def active_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: + limit_orders = [] + for market_pair, orders_map in self._tracked_limit_orders.items(): + for limit_order in orders_map.values(): + limit_orders.append((market_pair.market, limit_order)) + return limit_orders + + @property + def shadow_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: + limit_orders = [] + for market_pair, orders_map in self._shadow_tracked_limit_orders.items(): + for limit_order in orders_map.values(): + limit_orders.append((market_pair.market, limit_order)) + return limit_orders + + @property + def market_pair_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: + market_pair_to_orders = {} + market_pairs = self._tracked_limit_orders.keys() + for market_pair in market_pairs: + maker_orders = [] + for limit_order in self._tracked_limit_orders[market_pair].values(): + maker_orders.append(limit_order) + market_pair_to_orders[market_pair] = maker_orders + return market_pair_to_orders diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py new file mode 100644 index 0000000000..17ce7b502f --- /dev/null +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -0,0 +1,77 @@ +from typing import ( + List, + Tuple, +) + +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.pure_market_making_as import ( + PureMarketMakingASStrategy, + OrderBookAssetPriceDelegate, + APIAssetPriceDelegate, +) +from hummingbot.strategy.pure_market_making_as.pure_market_making_as_config_map import pure_market_making_as_config_map as c_map +from hummingbot.connector.exchange.paper_trade import create_paper_trade_market +from hummingbot.connector.exchange_base import ExchangeBase +from decimal import Decimal + + +def start(self): + try: + order_amount = c_map.get("order_amount").value + order_refresh_time = c_map.get("order_refresh_time").value + ping_pong_enabled = c_map.get("ping_pong_enabled").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') + add_transaction_costs_to_orders = c_map.get("add_transaction_costs").value + price_source = c_map.get("price_source").value + price_type = c_map.get("price_type").value + price_source_exchange = c_map.get("price_source_exchange").value + price_source_market = c_map.get("price_source_market").value + price_source_custom_api = c_map.get("price_source_custom_api").value + order_override = c_map.get("order_override").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)] + asset_price_delegate = None + if price_source == "external_market": + asset_trading_pair: str = price_source_market + ext_market = create_paper_trade_market(price_source_exchange, [asset_trading_pair]) + self.markets[price_source_exchange]: ExchangeBase = ext_market + asset_price_delegate = OrderBookAssetPriceDelegate(ext_market, asset_trading_pair) + elif price_source == "custom_api": + asset_price_delegate = APIAssetPriceDelegate(price_source_custom_api) + take_if_crossed = c_map.get("take_if_crossed").value + + strategy_logging_options = PureMarketMakingASStrategy.OPTION_LOG_ALL + kappa = c_map.get("kappa").value + gamma = c_map.get("gamma").value + closing_time = c_map.get("closing_time").value * 3600 * 24 * 1e3 + + self.strategy = PureMarketMakingASStrategy( + market_info=MarketTradingPairTuple(*maker_data), + order_amount=order_amount, + inventory_target_base_pct=inventory_target_base_pct, + order_refresh_time=order_refresh_time, + add_transaction_costs_to_orders=add_transaction_costs_to_orders, + logging_options=strategy_logging_options, + asset_price_delegate=asset_price_delegate, + price_type=price_type, + take_if_crossed=take_if_crossed, + ping_pong_enabled=ping_pong_enabled, + hb_app_notification=True, + order_override={} if order_override is None else order_override, + kappa=kappa, + gamma=gamma, + closing_time=closing_time, + ) + except Exception as e: + self._notify(str(e)) + self.logger().error("Unknown error during initialization.", exc_info=True) diff --git a/hummingbot/strategy/utils/__init__.py b/hummingbot/strategy/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hummingbot/strategy/utils/ring_buffer.pxd b/hummingbot/strategy/utils/ring_buffer.pxd new file mode 100644 index 0000000000..471a5f0dc3 --- /dev/null +++ b/hummingbot/strategy/utils/ring_buffer.pxd @@ -0,0 +1,21 @@ +import numpy as np +from libc.stdint cimport int64_t +cimport numpy as np + +cdef class RingBuffer: + cdef: + np.float64_t[:] _buffer + int64_t _start_index + int64_t _stop_index + int64_t _length + bint _is_full + + cdef c_add_value(self, float val) + cdef c_increment_index(self) + cdef double c_get_last_value(self) + cdef bint c_is_full(self) + cdef object c_get_array(self) + cdef double c_mean_value(self) + cdef double c_variance(self) + cdef double c_std_dev(self) + cdef object c_get_as_numpy_array(self) diff --git a/hummingbot/strategy/utils/ring_buffer.pyx b/hummingbot/strategy/utils/ring_buffer.pyx new file mode 100644 index 0000000000..17a7f10954 --- /dev/null +++ b/hummingbot/strategy/utils/ring_buffer.pyx @@ -0,0 +1,67 @@ +import numpy as np +import logging +cimport numpy as np + + +pmm_logger = None + +cdef class RingBuffer: + @classmethod + def logger(cls): + global pmm_logger + if pmm_logger is None: + pmm_logger = logging.getLogger(__name__) + return pmm_logger + + def __cinit__(self, int length): + self._length = length + self._buffer = np.zeros(length) + self._start_index = 0 + self._stop_index = 0 + self._is_full = False + + cdef c_add_value(self, float val): + self._buffer[self._stop_index] = val + self.c_increment_index() + + cdef c_increment_index(self): + self._stop_index = (self._stop_index + 1) % self._length + if(self._start_index == self._stop_index): + self._is_full = True + self._start_index = (self._start_index + 1) % self._length + + cdef double c_get_last_value(self): + if self._stop_index==0: + return self.c_get_array()[-1] + else: + return self.c_get_array()[self._stop_index-1] + + cdef bint c_is_full(self): + return self._is_full + + cdef object c_get_array(self): + return self._buffer + + cdef double c_mean_value(self): + result = np.nan + if self._is_full: + result=np.mean(self.c_get_as_numpy_array()) + return result + + cdef double c_variance(self): + result = np.nan + if self._is_full: + result = np.var(self.c_get_as_numpy_array()) + return result + + cdef double c_std_dev(self): + result = np.nan + if self._is_full: + result = np.std(self.c_get_as_numpy_array()) + return result + + cdef object c_get_as_numpy_array(self): + indexes = np.arange(self._start_index, stop=self._start_index + self._length) % self._length + if not self._is_full: + indexes = np.arange(self._start_index, stop=self._stop_index) + return np.asarray(self.c_get_array())[indexes] diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml new file mode 100644 index 0000000000..4c4616aece --- /dev/null +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -0,0 +1,63 @@ +######################################################## +### Pure market making strategy config ### +######################################################## + +template_version: 20 +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 + +# Size of your bid and ask order. +order_amount: null + +# Whether to alternate between buys and sells (true/false). +ping_pong_enabled: 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 + +# The price source (current_market/external_market/custom_api). +price_source: null + +# The price type (mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost). +price_type: null + +# An external exchange name (for external exchange pricing source). +price_source_exchange: null + +# A trading pair for the external exchange, e.g. BTC-USDT (for external exchange pricing source). +price_source_market: null + +# An external api that returns price (for custom_api pricing source). +price_source_custom_api: null + +#Take order if they cross order book when external price source is enabled +take_if_crossed: null + +# Use user provided orders to directly override the orders placed by order_amount and order_level_parameter +# This is an advanced feature and user is expected to directly edit this field in config file +# Below is an sample input, the format is a dictionary, the key is user-defined order name, the value is a list which includes buy/sell, order spread, and order amount +# order_override: +# order_1: [buy, 0.5, 100] +# order_2: [buy, 0.75, 200] +# order_3: [sell, 0.1, 500] +# Please make sure there is a space between : and [ +order_override: null + +kappa: null +gamma: null +closing_time: null + +# For more detailed information, see: +# https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters From 50a380f7ab3941ad085601ebede7c7f2a42c4b8e Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 15 Feb 2021 23:09:33 -0300 Subject: [PATCH 02/77] bug fixes. Added more verbosity and csv log --- .../pure_market_making_as.pxd | 1 + .../pure_market_making_as.pyx | 131 ++++++++++-------- .../pure_market_making_as_config_map.py | 22 +++ .../strategy/pure_market_making_as/start.py | 7 +- ...ure_market_making_as_strategy_TEMPLATE.yml | 7 + 5 files changed, 109 insertions(+), 59 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index 39b9e8992a..0f103c1e37 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -64,6 +64,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double _optimal_ask RingBuffer _mid_prices RingBuffer _spreads + str _csv_path cdef object c_get_mid_price(self) cdef object c_create_base_proposal(self) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index cc99bed70a..80dde3dac2 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -65,6 +65,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): order_amount: Decimal, order_refresh_time: float = 30.0, max_order_age = 1800.0, + order_refresh_tolerance_pct: Decimal = s_decimal_neg_one, inventory_target_base_pct: Decimal = s_decimal_zero, add_transaction_costs_to_orders: bool = False, asset_price_delegate: AssetPriceDelegate = None, @@ -80,13 +81,16 @@ cdef class PureMarketMakingASStrategy(StrategyBase): kappa: float = 0.1, gamma: float = 0.5, closing_time: float = 3600.0 * 24 * 1e3, + data_path: str = '', ): super().__init__() self._sb_order_tracker = PureMarketMakingASOrderTracker() self._market_info = market_info self._order_amount = order_amount + self._order_level_spread = 0 self._order_refresh_time = order_refresh_time self._max_order_age = max_order_age + self._order_refresh_tolerance_pct = order_refresh_tolerance_pct self._inventory_target_base_pct = inventory_target_base_pct self._add_transaction_costs_to_orders = add_transaction_costs_to_orders self._asset_price_delegate = asset_price_delegate @@ -115,8 +119,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) - self._mid_prices = RingBuffer(5) - self._spreads = RingBuffer(5) + self._mid_prices = RingBuffer(30) + self._spreads = RingBuffer(30) self._kappa = kappa self._gamma = gamma self._time_left = closing_time @@ -126,6 +130,12 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_ask = 0 self._optimal_bid = 0 + self._csv_path = os.path.join(data_path, "PMM_AS.csv") + try: + os.unlink(self._csv_path) + except FileNotFoundError: + pass + def all_markets_ready(self): return all([market.ready for market in self._sb_markets]) @@ -454,9 +464,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 = "" if level is None else self._order_amount + ((level - 1) * self._order_level_amount) + amount_orig = "" data.append([ - "hang" if order.client_order_id in self._hanging_order_ids else level, + "", "buy" if order.is_buy else "sell", float(order.price), f"{spread:.2%}", @@ -581,22 +591,25 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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.") - csv_filename = "PMM_AS.csv" - csv_path = '/Users/nicolas/Desktop/'+csv_filename self.c_collect_market_variables(timestamp) + algo_inform_text = "Algorithm not ready" if self.c_is_algorithm_ready(): self.c_calculate_reserved_price_and_optimal_spread() - if not os.path.exists(csv_path): + algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.c_get_last_value())/self._mid_prices.c_get_last_value()*100.0}% | " \ + f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.c_get_last_value())/self._spreads.c_get_last_value()*100.0}% | " \ + f"q={self.c_calculate_target_inventory()} | " \ + f"(T-t)={self._time_left/self._closing_time}" + if not os.path.exists(self._csv_path): df_header = pd.DataFrame([('mid_price', 'spread', 'reserved_price', 'optimal_spread', 'q', 'time_left_fraction', - 'std_dev', + 'mid_price std_dev', 'gamma', 'kappa')]) - df_header.to_csv(csv_path, mode='a', header=False, index=False) + df_header.to_csv(self._csv_path, mode='a', header=False, index=False) df = pd.DataFrame([(self._mid_prices.c_get_last_value(), self._spreads.c_get_last_value(), self._reserved_price, @@ -606,38 +619,39 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._mid_prices.c_std_dev(), self._gamma, self._kappa)]) - df.to_csv(csv_path, mode='a', header=False, index=False) - - proposal = None - asset_mid_price = Decimal("0") - # asset_mid_price = self.c_set_mid_price(market_info) - if self._create_timestamp <= self._current_timestamp: - # 1. Create base order proposals - proposal = self.c_create_base_proposal() - # 2. Apply functions that limit numbers of buys and sells proposal - self.c_apply_order_levels_modifiers(proposal) - # 3. Apply functions that modify orders price - self.c_apply_order_price_modifiers(proposal) - # 5. Apply budget constraint, i.e. can't buy/sell more than what you have. - self.c_apply_budget_constraint(proposal) - - if not self._take_if_crossed: - self.c_filter_out_takers(proposal) - self.c_cancel_active_orders(proposal) - self.c_cancel_hanging_orders() - 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) + df.to_csv(self._csv_path, mode='a', header=False, index=False) + + proposal = None + asset_mid_price = Decimal("0") + # asset_mid_price = self.c_set_mid_price(market_info) + if self._create_timestamp <= self._current_timestamp: + # 1. Create base order proposals + proposal = self.c_create_base_proposal() + # 2. Apply functions that limit numbers of buys and sells proposal + self.c_apply_order_levels_modifiers(proposal) + # 3. Apply functions that modify orders price + self.c_apply_order_price_modifiers(proposal) + # 5. Apply budget constraint, i.e. can't buy/sell more than what you have. + self.c_apply_budget_constraint(proposal) + + if not self._take_if_crossed: + self.c_filter_out_takers(proposal) + self.c_cancel_active_orders(proposal) + self.c_cancel_hanging_orders() + 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) + self.logger().info(algo_inform_text) finally: self._last_timestamp = timestamp cdef c_collect_market_variables(self, double timestamp): self.c_save_mid_price() self.c_save_spread() - self._time_left = max(self._closing_time - (timestamp-self._last_timestamp), 0) + self._time_left = max(self._time_left - (timestamp-self._last_timestamp), 0) cdef c_save_mid_price(self): self._mid_prices.c_add_value(self.c_get_mid_price()) @@ -685,13 +699,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): mid_price = self._mid_prices.c_get_last_value() # Need to review this to see if adjusted quantities are required - base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) + base_asset_amount = market.c_get_available_balance(self.base_asset) + quote_asset_amount = market.c_get_available_balance(self.quote_asset) base_value = float(base_asset_amount) * mid_price inventory_value = base_value + float(quote_asset_amount) target_inventory_value = inventory_value * float(self._inventory_target_base_pct) q = market.c_quantize_order_amount(trading_pair, target_inventory_value / mid_price) - self.logger().info(f"mid:{mid_price} | base_amt:{float(base_asset_amount)} | base_value:{base_value} | inv_value:{inventory_value} | q_value: {target_inventory_value} | q:{q}") + # self.logger().info(f"market: {market.display_name} | mid:{mid_price} | base_amt:{float(base_asset_amount)} | base_value:{base_value} | inv_value:{inventory_value} | q_value: {target_inventory_value} | q:{q}") return q @@ -705,7 +720,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): list sells = [] base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) - delta_quantity = float(base_asset_amount) - self.c_calculate_target_inventory() + delta_quantity = self.c_calculate_target_inventory() - float(base_asset_amount) self.logger().info(f"delta_quantity:{delta_quantity}") @@ -719,7 +734,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if delta_quantity < 0: price = self._reserved_price + self._optimal_spread / 2 price = market.c_quantize_order_price(self.trading_pair, price) - size = market.c_quantize_order_amount(self.trading_pair, delta_quantity) + size = market.c_quantize_order_amount(self.trading_pair, -delta_quantity) if size>0: sells.append(PriceSize(price, size)) @@ -770,8 +785,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ) cdef c_apply_order_price_modifiers(self, object proposal): - if self._order_optimization_enabled: - self.c_apply_order_optimization(proposal) + self.c_apply_order_optimization(proposal) if self._add_transaction_costs_to_orders: self.c_apply_add_transaction_costs(proposal) @@ -840,6 +854,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ExchangeBase market = self._market_info.market object own_buy_size = s_decimal_zero object own_sell_size = s_decimal_zero + double best_order_spread for order in self.active_orders: if order.is_buy: @@ -847,10 +862,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): else: own_sell_size = order.quantity + # 10% of Bid/Ask spread + best_order_spread = self._optimal_spread / 2 * 0.1 + 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, self._bid_order_optimization_depth + own_buy_size).result_price + False, own_buy_size).result_price price_quantum = market.c_get_order_price_quantum( self.trading_pair, top_bid_price @@ -859,16 +877,16 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 order_level_spread to each order in the next levels + # 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) * (1 - self.order_level_spread * i) + proposal.buys[i].price = market.c_quantize_order_price(self.trading_pair, lower_buy_price * Decimal(str(1 - best_order_spread * i))) 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, self._ask_order_optimization_depth + own_sell_size).result_price + True, own_sell_size).result_price price_quantum = market.c_get_order_price_quantum( self.trading_pair, top_ask_price @@ -877,11 +895,11 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 order_level_spread to each order in the next levels + # 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) * (1 + self.order_level_spread * i) + proposal.sells[i].price = market.c_quantize_order_price(self.trading_pair, higher_sell_price * Decimal(str(1 + best_order_spread * i))) cdef object c_apply_add_transaction_costs(self, object proposal): cdef: @@ -1015,16 +1033,15 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ) cdef bint c_is_within_tolerance(self, list current_prices, list proposal_prices): - return False - # 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 + 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 non hanging orders # Return value: whether order cancellation is deferred. diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index d9d8d7ff3f..b522d23207 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -11,6 +11,10 @@ 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, ) @@ -137,15 +141,33 @@ def exchange_on_validated(value: str): 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_amount": ConfigVar(key="order_amount", prompt=order_amount_prompt, type_str="decimal", validator=validate_order_amount, prompt_on_new=True), + "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)), "ping_pong_enabled": ConfigVar(key="ping_pong_enabled", prompt="Would you like to use the ping pong feature and alternate between buy and sell orders after fills? (Yes/No) >>> ", diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index 17ce7b502f..c44c84055b 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -3,6 +3,7 @@ Tuple, ) +from hummingbot import data_path from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple from hummingbot.strategy.pure_market_making_as import ( PureMarketMakingASStrategy, @@ -24,12 +25,12 @@ def start(self): 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') - add_transaction_costs_to_orders = c_map.get("add_transaction_costs").value price_source = c_map.get("price_source").value price_type = c_map.get("price_type").value price_source_exchange = c_map.get("price_source_exchange").value price_source_market = c_map.get("price_source_market").value price_source_custom_api = c_map.get("price_source_custom_api").value + order_refresh_tolerance_pct = c_map.get("order_refresh_tolerance_pct").value / Decimal('100') order_override = c_map.get("order_override").value trading_pair: str = raw_trading_pair @@ -60,7 +61,8 @@ def start(self): order_amount=order_amount, inventory_target_base_pct=inventory_target_base_pct, order_refresh_time=order_refresh_time, - add_transaction_costs_to_orders=add_transaction_costs_to_orders, + order_refresh_tolerance_pct=order_refresh_tolerance_pct, + add_transaction_costs_to_orders=True, logging_options=strategy_logging_options, asset_price_delegate=asset_price_delegate, price_type=price_type, @@ -71,6 +73,7 @@ def start(self): kappa=kappa, gamma=gamma, closing_time=closing_time, + data_path=data_path(), ) except Exception as e: self._notify(str(e)) diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 4c4616aece..740e51f222 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -15,6 +15,13 @@ market: null # If the value is 60, the bot cancels active orders and placing new ones after a minute. order_refresh_time: null +# 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 From f844af2642bab6582298d37d82ac4813bb9b38bf Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Tue, 16 Feb 2021 13:41:36 -0300 Subject: [PATCH 03/77] Fixed bug with time_left variable --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 80dde3dac2..5899a59add 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -651,7 +651,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef c_collect_market_variables(self, double timestamp): self.c_save_mid_price() self.c_save_spread() - self._time_left = max(self._time_left - (timestamp-self._last_timestamp), 0) + self._time_left = max(self._time_left - (timestamp-self._last_timestamp)*1e3, 0) cdef c_save_mid_price(self): self._mid_prices.c_add_value(self.c_get_mid_price()) From 92288a2a7a380db7ad1a01a3cda9f927107f3d90 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 17 Feb 2021 02:49:45 -0300 Subject: [PATCH 04/77] Removed additional parameters. Added switch to enable fixed_order_amount or dynamic --- .../pure_market_making_as.pxd | 7 +- .../pure_market_making_as.pyx | 113 ++++++------------ .../pure_market_making_as_config_map.py | 33 ++--- .../strategy/pure_market_making_as/start.py | 8 +- ...ure_market_making_as_strategy_TEMPLATE.yml | 17 +-- 5 files changed, 50 insertions(+), 128 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index 0f103c1e37..b8cbaf8b25 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -13,6 +13,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object _ask_spread object _minimum_spread object _order_amount + bint _fixed_order_amount int _order_levels int _buy_levels int _sell_levels @@ -34,13 +35,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object _asset_price_delegate object _inventory_cost_price_delegate object _price_type - bint _take_if_crossed object _price_ceiling object _price_floor - bint _ping_pong_enabled - list _ping_pong_warning_lines bint _hb_app_notification - object _order_override double _cancel_timestamp double _create_timestamp @@ -71,8 +68,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef tuple c_get_adjusted_available_balance(self, list orders) cdef c_apply_order_levels_modifiers(self, object proposal) cdef c_apply_price_band(self, object proposal) - cdef c_apply_ping_pong(self, object proposal) cdef c_apply_order_price_modifiers(self, object proposal) + cdef c_apply_order_amount_constraint(self, object proposal) cdef c_apply_budget_constraint(self, object proposal) cdef c_filter_out_takers(self, object proposal) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 5899a59add..9fb12c5e13 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -67,26 +67,25 @@ cdef class PureMarketMakingASStrategy(StrategyBase): max_order_age = 1800.0, order_refresh_tolerance_pct: Decimal = s_decimal_neg_one, inventory_target_base_pct: Decimal = s_decimal_zero, - add_transaction_costs_to_orders: bool = False, + add_transaction_costs_to_orders: bool = True, asset_price_delegate: AssetPriceDelegate = None, price_type: str = "mid_price", - take_if_crossed: bool = False, price_ceiling: Decimal = s_decimal_neg_one, price_floor: Decimal = s_decimal_neg_one, - ping_pong_enabled: bool = False, logging_options: int = OPTION_LOG_ALL, status_report_interval: float = 900, hb_app_notification: bool = False, - order_override: Dict[str, List[str]] = {}, kappa: float = 0.1, gamma: float = 0.5, closing_time: float = 3600.0 * 24 * 1e3, + fixed_order_amount: bool = False, data_path: str = '', ): super().__init__() self._sb_order_tracker = PureMarketMakingASOrderTracker() self._market_info = market_info self._order_amount = order_amount + self._fixed_order_amount = fixed_order_amount self._order_level_spread = 0 self._order_refresh_time = order_refresh_time self._max_order_age = max_order_age @@ -95,20 +94,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._add_transaction_costs_to_orders = add_transaction_costs_to_orders self._asset_price_delegate = asset_price_delegate self._price_type = self.get_price_type(price_type) - self._take_if_crossed = take_if_crossed self._price_ceiling = price_ceiling self._price_floor = price_floor - self._ping_pong_enabled = ping_pong_enabled - self._ping_pong_warning_lines = [] self._hb_app_notification = hb_app_notification - self._order_override = order_override self._cancel_timestamp = 0 self._create_timestamp = 0 self._hanging_aged_order_prices = [] self._limit_order_type = self._market_info.market.get_maker_order_type() - if take_if_crossed: - self._limit_order_type = OrderType.LIMIT self._all_markets_ready = False self._filled_buys_balance = 0 self._filled_sells_balance = 0 @@ -119,8 +112,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) - self._mid_prices = RingBuffer(30) - self._spreads = RingBuffer(30) + self._mid_prices = RingBuffer(int(order_refresh_time)) + self._spreads = RingBuffer(int(order_refresh_time)) self._kappa = kappa self._gamma = gamma self._time_left = closing_time @@ -201,14 +194,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def order_level_spread(self, value: Decimal): self._order_level_spread = value - @property - def inventory_skew_enabled(self) -> bool: - return self._inventory_skew_enabled - - @inventory_skew_enabled.setter - def inventory_skew_enabled(self, value: bool): - self._inventory_skew_enabled = value - @property def inventory_target_base_pct(self) -> Decimal: return self._inventory_target_base_pct @@ -241,22 +226,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def hanging_orders_cancel_pct(self, value: Decimal): self._hanging_orders_cancel_pct = value - @property - def bid_spread(self) -> Decimal: - return self._bid_spread - - @bid_spread.setter - def bid_spread(self, value: Decimal): - self._bid_spread = value - - @property - def ask_spread(self) -> Decimal: - return self._ask_spread - - @ask_spread.setter - def ask_spread(self, value: Decimal): - self._ask_spread = value - @property def order_optimization_enabled(self) -> bool: return self._order_optimization_enabled @@ -325,14 +294,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def trading_pair(self): return self._market_info.trading_pair - @property - def order_override(self): - return self._order_override - - @order_override.setter - def order_override(self, value: Dict[str, List[str]]): - self._order_override = value - def get_price(self) -> float: price_provider = self._asset_price_delegate or self._market_info if self._price_type is PriceType.LastOwnTrade: @@ -441,6 +402,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 not in self._hanging_order_ids]) @@ -464,7 +426,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 = "" + + amount_orig = np.abs(self.c_calculate_target_inventory() - float(market.get_balance(base_asset))) data.append([ "", "buy" if order.is_buy else "sell", @@ -517,18 +480,12 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef: list lines = [] list warning_lines = [] - warning_lines.extend(self._ping_pong_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(not self._inventory_skew_enabled) - # append inventory skew stats. - if self._inventory_skew_enabled: - inventory_skew_df = self.inventory_skew_stats_data_frame() - assets_df = assets_df.append(inventory_skew_df) - + 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") @@ -597,14 +554,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_calculate_reserved_price_and_optimal_spread() algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.c_get_last_value())/self._mid_prices.c_get_last_value()*100.0}% | " \ f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.c_get_last_value())/self._spreads.c_get_last_value()*100.0}% | " \ - f"q={self.c_calculate_target_inventory()} | " \ + f"target_inv_stocks={self.c_calculate_target_inventory()} | " \ f"(T-t)={self._time_left/self._closing_time}" if not os.path.exists(self._csv_path): df_header = pd.DataFrame([('mid_price', 'spread', 'reserved_price', 'optimal_spread', - 'q', + 'target_inv_stocks', 'time_left_fraction', 'mid_price std_dev', 'gamma', @@ -634,8 +591,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): # 5. Apply budget constraint, i.e. can't buy/sell more than what you have. self.c_apply_budget_constraint(proposal) - if not self._take_if_crossed: - self.c_filter_out_takers(proposal) self.c_cancel_active_orders(proposal) self.c_cancel_hanging_orders() refresh_proposal = self.c_aged_order_refresh() @@ -652,6 +607,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_save_mid_price() self.c_save_spread() self._time_left = max(self._time_left - (timestamp-self._last_timestamp)*1e3, 0) + if self._time_left == 0: + self._time_left = self._closing_time # Re-cycle algorithm + self.logger().info("Recycling algorithm time left...") cdef c_save_mid_price(self): self._mid_prices.c_add_value(self.c_get_mid_price()) @@ -677,7 +635,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self.c_is_algorithm_ready(): mid_price = self._mid_prices.c_get_last_value() - q = float(self.c_calculate_target_inventory()) + q = float(market.c_get_available_balance(self.base_asset)) mid_price_variance = self._mid_prices.c_variance() self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) @@ -695,7 +653,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double base_value double inventory_value double target_inventory_value - double q + double N mid_price = self._mid_prices.c_get_last_value() # Need to review this to see if adjusted quantities are required @@ -704,11 +662,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): base_value = float(base_asset_amount) * mid_price inventory_value = base_value + float(quote_asset_amount) target_inventory_value = inventory_value * float(self._inventory_target_base_pct) - q = market.c_quantize_order_amount(trading_pair, target_inventory_value / mid_price) + N = market.c_quantize_order_amount(trading_pair, target_inventory_value / mid_price) - # self.logger().info(f"market: {market.display_name} | mid:{mid_price} | base_amt:{float(base_asset_amount)} | base_value:{base_value} | inv_value:{inventory_value} | q_value: {target_inventory_value} | q:{q}") - - return q + return N cdef bint c_is_algorithm_ready(self): return self._mid_prices.c_is_full() @@ -760,8 +716,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef c_apply_order_levels_modifiers(self, proposal): self.c_apply_price_band(proposal) - if self._ping_pong_enabled: - self.c_apply_ping_pong(proposal) cdef c_apply_price_band(self, proposal): if self._price_ceiling > 0 and self.get_price() >= self._price_ceiling: @@ -769,27 +723,28 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self._price_floor > 0 and self.get_price() <= self._price_floor: proposal.sells = [] - cdef c_apply_ping_pong(self, object proposal): - self._ping_pong_warning_lines = [] - if self._filled_buys_balance == self._filled_sells_balance: - self._filled_buys_balance = self._filled_sells_balance = 0 - if self._filled_buys_balance > 0: - proposal.buys = proposal.buys[self._filled_buys_balance:] - self._ping_pong_warning_lines.extend( - [f" Ping-pong removed {self._filled_buys_balance} buy orders."] - ) - if self._filled_sells_balance > 0: - proposal.sells = proposal.sells[self._filled_sells_balance:] - self._ping_pong_warning_lines.extend( - [f" Ping-pong removed {self._filled_sells_balance} sell orders."] - ) - cdef c_apply_order_price_modifiers(self, object proposal): self.c_apply_order_optimization(proposal) + if self._fixed_order_amount: + self.c_apply_order_amount_constraint(proposal) if self._add_transaction_costs_to_orders: self.c_apply_add_transaction_costs(proposal) + cdef c_apply_order_amount_constraint(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + + for buy in proposal.buys: + buy.size = s_decimal_zero if buy.size < self._order_amount else self._order_amount + + proposal.buys = [o for o in proposal.buys if o.size > 0] + + for sell in proposal.sells: + sell.size = s_decimal_zero if sell.size < self._order_amount else self._order_amount + + proposal.sells = [o for o in proposal.sells if o.size > 0] + cdef c_apply_budget_constraint(self, object proposal): cdef: ExchangeBase market = self._market_info.market diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index b522d23207..d35c5b0e22 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -62,7 +62,6 @@ def on_validate_price_source(value: str): if value != "external_market": pure_market_making_as_config_map["price_source_exchange"].value = None pure_market_making_as_config_map["price_source_market"].value = None - pure_market_making_as_config_map["take_if_crossed"].value = None if value != "custom_api": pure_market_making_as_config_map["price_source_custom_api"].value = None else: @@ -123,13 +122,13 @@ def exchange_on_validated(value: str): ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 10000, inclusive=False), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 10000, inclusive=False), prompt_on_new=True), "closing_time": ConfigVar(key="closing_time", @@ -155,9 +154,17 @@ def exchange_on_validated(value: str): type_str="float", default=Decimal("1800"), validator=lambda v: validate_decimal(v, 0, inclusive=False)), + "fixed_order_amount": + ConfigVar(key="fixed_order_amount", + prompt="Do you want to create orders with fixed amount? (Alternative is to leave algorithm decide) >>>", + type_str="bool", + default=False, + validator=validate_bool, + prompt_on_new=True), "order_amount": ConfigVar(key="order_amount", prompt=order_amount_prompt, + required_if=lambda: pure_market_making_as_config_map.get("fixed_order_amount").value == "True", type_str="decimal", validator=validate_order_amount, prompt_on_new=True), @@ -168,13 +175,6 @@ def exchange_on_validated(value: str): type_str="decimal", default=Decimal("0"), validator=lambda v: validate_decimal(v, -10, 10, inclusive=True)), - "ping_pong_enabled": - ConfigVar(key="ping_pong_enabled", - prompt="Would you like to use the ping pong feature and alternate between buy and sell orders after fills? (Yes/No) >>> ", - type_str="bool", - default=False, - prompt_on_new=True, - validator=validate_bool), "inventory_target_base_pct": ConfigVar(key="inventory_target_base_pct", prompt="What is your target base asset percentage? Enter 50 for 50% >>> ", @@ -222,22 +222,9 @@ def exchange_on_validated(value: str): required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "external_market", type_str="str", validator=validate_price_source_market), - "take_if_crossed": - ConfigVar(key="take_if_crossed", - prompt="Do you want to take the best order if orders cross the orderbook? ((Yes/No) >>> ", - required_if=lambda: pure_market_making_as_config_map.get( - "price_source").value == "external_market", - type_str="bool", - validator=validate_bool), "price_source_custom_api": ConfigVar(key="price_source_custom_api", prompt="Enter pricing API URL >>> ", required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "custom_api", type_str="str"), - "order_override": - ConfigVar(key="order_override", - prompt=None, - required_if=lambda: False, - default=None, - type_str="json"), } diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index c44c84055b..5f8a3a15d2 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -20,7 +20,6 @@ def start(self): try: order_amount = c_map.get("order_amount").value order_refresh_time = c_map.get("order_refresh_time").value - ping_pong_enabled = c_map.get("ping_pong_enabled").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 \ @@ -31,7 +30,6 @@ def start(self): price_source_market = c_map.get("price_source_market").value price_source_custom_api = c_map.get("price_source_custom_api").value order_refresh_tolerance_pct = c_map.get("order_refresh_tolerance_pct").value / Decimal('100') - order_override = c_map.get("order_override").value trading_pair: str = raw_trading_pair maker_assets: Tuple[str, str] = self._initialize_market_assets(exchange, [trading_pair])[0] @@ -49,12 +47,12 @@ def start(self): asset_price_delegate = OrderBookAssetPriceDelegate(ext_market, asset_trading_pair) elif price_source == "custom_api": asset_price_delegate = APIAssetPriceDelegate(price_source_custom_api) - take_if_crossed = c_map.get("take_if_crossed").value strategy_logging_options = PureMarketMakingASStrategy.OPTION_LOG_ALL kappa = c_map.get("kappa").value gamma = c_map.get("gamma").value closing_time = c_map.get("closing_time").value * 3600 * 24 * 1e3 + fixed_order_amount = c_map.get("fixed_order_amount").value self.strategy = PureMarketMakingASStrategy( market_info=MarketTradingPairTuple(*maker_data), @@ -66,13 +64,11 @@ def start(self): logging_options=strategy_logging_options, asset_price_delegate=asset_price_delegate, price_type=price_type, - take_if_crossed=take_if_crossed, - ping_pong_enabled=ping_pong_enabled, hb_app_notification=True, - order_override={} if order_override is None else order_override, kappa=kappa, gamma=gamma, closing_time=closing_time, + fixed_order_amount=fixed_order_amount, data_path=data_path(), ) except Exception as e: diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 740e51f222..91772b93ee 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -25,8 +25,8 @@ order_refresh_tolerance_pct: null # Size of your bid and ask order. order_amount: null -# Whether to alternate between buys and sells (true/false). -ping_pong_enabled: null +# Size of your bid and ask order. +fixed_order_amount: null # Target base asset inventory percentage target to be maintained (for Inventory skew feature). inventory_target_base_pct: null @@ -49,19 +49,6 @@ price_source_market: null # An external api that returns price (for custom_api pricing source). price_source_custom_api: null -#Take order if they cross order book when external price source is enabled -take_if_crossed: null - -# Use user provided orders to directly override the orders placed by order_amount and order_level_parameter -# This is an advanced feature and user is expected to directly edit this field in config file -# Below is an sample input, the format is a dictionary, the key is user-defined order name, the value is a list which includes buy/sell, order spread, and order amount -# order_override: -# order_1: [buy, 0.5, 100] -# order_2: [buy, 0.75, 200] -# order_3: [sell, 0.1, 500] -# Please make sure there is a space between : and [ -order_override: null - kappa: null gamma: null closing_time: null From a96a64006d9e59267d9c710f7b4f1cc32d15c954 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 17 Feb 2021 19:24:13 -0300 Subject: [PATCH 05/77] Moved buffer_size as a configurable parameter --- .../pure_market_making_as/pure_market_making_as.pyx | 5 +++-- .../pure_market_making_as_config_map.py | 6 ++++++ hummingbot/strategy/pure_market_making_as/start.py | 2 ++ .../conf_pure_market_making_as_strategy_TEMPLATE.yml | 6 +++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 9fb12c5e13..1aba52c8b3 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -80,6 +80,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): closing_time: float = 3600.0 * 24 * 1e3, fixed_order_amount: bool = False, data_path: str = '', + buffer_size: int = 30, ): super().__init__() self._sb_order_tracker = PureMarketMakingASOrderTracker() @@ -112,8 +113,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) - self._mid_prices = RingBuffer(int(order_refresh_time)) - self._spreads = RingBuffer(int(order_refresh_time)) + self._mid_prices = RingBuffer(buffer_size) + self._spreads = RingBuffer(buffer_size) self._kappa = kappa self._gamma = gamma self._time_left = closing_time diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index d35c5b0e22..7f8c720f2b 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -227,4 +227,10 @@ def exchange_on_validated(value: str): prompt="Enter pricing API URL >>> ", required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "custom_api", type_str="str"), + "buffer_size": + ConfigVar(key="buffer_size", + prompt="Enter amount of samples to use for volatility calculation>>> ", + type_str="int", + validator=lambda v: validate_decimal(v, 5, 600), + default=Decimal("30")), } diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index 5f8a3a15d2..327be7de6d 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -53,6 +53,7 @@ def start(self): gamma = c_map.get("gamma").value closing_time = c_map.get("closing_time").value * 3600 * 24 * 1e3 fixed_order_amount = c_map.get("fixed_order_amount").value + buffer_size = c_map.get("buffer_size").value self.strategy = PureMarketMakingASStrategy( market_info=MarketTradingPairTuple(*maker_data), @@ -70,6 +71,7 @@ def start(self): closing_time=closing_time, fixed_order_amount=fixed_order_amount, data_path=data_path(), + buffer_size = buffer_size, ) except Exception as e: self._notify(str(e)) diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 91772b93ee..12f6ae34f1 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -2,7 +2,7 @@ ### Pure market making strategy config ### ######################################################## -template_version: 20 +template_version: 1 strategy: null # Exchange and token parameters. @@ -49,9 +49,13 @@ price_source_market: null # An external api that returns price (for custom_api pricing source). price_source_custom_api: null +# Avellaneda - Stoikov algorithm parameters kappa: null gamma: null closing_time: null +# Buffer size used to store historic samples and calculate volatility +buffer_size: 30 + # For more detailed information, see: # https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters From 8b971a1fb7fa479f4ed3bbf1c0d63be12850f92f Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 17 Feb 2021 20:19:57 -0300 Subject: [PATCH 06/77] Changed debug messages and csv dump to correctly reflect q --- .../pure_market_making_as/pure_market_making_as.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 1aba52c8b3..1365f90f5c 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -124,7 +124,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_ask = 0 self._optimal_bid = 0 - self._csv_path = os.path.join(data_path, "PMM_AS.csv") + self._csv_path = os.path.join(data_path, f"PMM_AS_{pd.Timestamp.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv") try: os.unlink(self._csv_path) except FileNotFoundError: @@ -555,14 +555,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_calculate_reserved_price_and_optimal_spread() algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.c_get_last_value())/self._mid_prices.c_get_last_value()*100.0}% | " \ f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.c_get_last_value())/self._spreads.c_get_last_value()*100.0}% | " \ - f"target_inv_stocks={self.c_calculate_target_inventory()} | " \ + f"q={self._market_info.market.c_get_available_balance(self.base_asset)} | " \ f"(T-t)={self._time_left/self._closing_time}" if not os.path.exists(self._csv_path): df_header = pd.DataFrame([('mid_price', 'spread', 'reserved_price', 'optimal_spread', - 'target_inv_stocks', + 'q', 'time_left_fraction', 'mid_price std_dev', 'gamma', @@ -572,7 +572,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._spreads.c_get_last_value(), self._reserved_price, self._optimal_spread, - self.c_calculate_target_inventory(), + self._market_info.market.c_get_available_balance(self.base_asset), self._time_left/self._closing_time, self._mid_prices.c_std_dev(), self._gamma, From 987d48d4e3ba875f2a235a5e5a5d18a9b2d2ab36 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 15:12:37 -0300 Subject: [PATCH 07/77] Changed RingBuffer class to be compatible with Pure Python. Fixed bug in debug messages --- .../pure_market_making_as.pyx | 35 +++++++-------- hummingbot/strategy/utils/ring_buffer.pxd | 19 ++++---- hummingbot/strategy/utils/ring_buffer.pyx | 43 ++++++++++--------- 3 files changed, 49 insertions(+), 48 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 1365f90f5c..16d3a95702 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -533,9 +533,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): bint should_report_warnings = ((current_tick > last_tick) and (self._logging_options & self.OPTION_LOG_STATUS_REPORT)) cdef object proposal + ExchangeBase market = self._market_info.market try: if not self._all_markets_ready: - self._all_markets_ready = all([market.ready for market in self._sb_markets]) + self._all_markets_ready = all([mkt.ready for mkt in self._sb_markets]) if self._asset_price_delegate is not None and self._all_markets_ready: self._all_markets_ready = self._asset_price_delegate.ready if not self._all_markets_ready: @@ -545,7 +546,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return if should_report_warnings: - if not all([market.network_status is NetworkStatus.CONNECTED for market in self._sb_markets]): + 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.") @@ -553,9 +554,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): algo_inform_text = "Algorithm not ready" if self.c_is_algorithm_ready(): self.c_calculate_reserved_price_and_optimal_spread() - algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.c_get_last_value())/self._mid_prices.c_get_last_value()*100.0}% | " \ - f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.c_get_last_value())/self._spreads.c_get_last_value()*100.0}% | " \ - f"q={self._market_info.market.c_get_available_balance(self.base_asset)} | " \ + algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ + f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.get_last_value()) / self._spreads.get_last_value() * 100.0}% | " \ + f"q={market.c_get_available_balance(self.base_asset)} | " \ f"(T-t)={self._time_left/self._closing_time}" if not os.path.exists(self._csv_path): df_header = pd.DataFrame([('mid_price', @@ -568,13 +569,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'gamma', 'kappa')]) df_header.to_csv(self._csv_path, mode='a', header=False, index=False) - df = pd.DataFrame([(self._mid_prices.c_get_last_value(), - self._spreads.c_get_last_value(), + df = pd.DataFrame([(self._mid_prices.get_last_value(), + self._spreads.get_last_value(), self._reserved_price, self._optimal_spread, - self._market_info.market.c_get_available_balance(self.base_asset), + market.c_get_available_balance(self.base_asset), self._time_left/self._closing_time, - self._mid_prices.c_std_dev(), + self._mid_prices.std_dev(), self._gamma, self._kappa)]) df.to_csv(self._csv_path, mode='a', header=False, index=False) @@ -613,10 +614,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.logger().info("Recycling algorithm time left...") cdef c_save_mid_price(self): - self._mid_prices.c_add_value(self.c_get_mid_price()) + self._mid_prices.add_value(self.c_get_mid_price()) cdef c_save_spread(self): - self._spreads.c_add_value(self.c_get_spread()) + self._spreads.add_value(self.c_get_spread()) cdef double c_get_spread(self): cdef: @@ -635,9 +636,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double buy_fee if self.c_is_algorithm_ready(): - mid_price = self._mid_prices.c_get_last_value() + mid_price = self._mid_prices.get_last_value() q = float(market.c_get_available_balance(self.base_asset)) - mid_price_variance = self._mid_prices.c_variance() + mid_price_variance = self._mid_prices.variance() self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + np.log(1 + self._gamma / self._kappa) @@ -656,10 +657,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double target_inventory_value double N - mid_price = self._mid_prices.c_get_last_value() + mid_price = self._mid_prices.get_last_value() # Need to review this to see if adjusted quantities are required - base_asset_amount = market.c_get_available_balance(self.base_asset) - quote_asset_amount = market.c_get_available_balance(self.quote_asset) + base_asset_amount = market.c_get_available_balance(base_asset) + quote_asset_amount = market.c_get_available_balance(quote_asset) base_value = float(base_asset_amount) * mid_price inventory_value = base_value + float(quote_asset_amount) target_inventory_value = inventory_value * float(self._inventory_target_base_pct) @@ -668,7 +669,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return N cdef bint c_is_algorithm_ready(self): - return self._mid_prices.c_is_full() + return self._mid_prices.is_full() cdef object c_create_base_proposal(self): cdef: diff --git a/hummingbot/strategy/utils/ring_buffer.pxd b/hummingbot/strategy/utils/ring_buffer.pxd index 471a5f0dc3..2bbc1da4ea 100644 --- a/hummingbot/strategy/utils/ring_buffer.pxd +++ b/hummingbot/strategy/utils/ring_buffer.pxd @@ -4,18 +4,17 @@ cimport numpy as np cdef class RingBuffer: cdef: - np.float64_t[:] _buffer + np.double_t[:] _buffer int64_t _start_index int64_t _stop_index int64_t _length bint _is_full - cdef c_add_value(self, float val) - cdef c_increment_index(self) - cdef double c_get_last_value(self) - cdef bint c_is_full(self) - cdef object c_get_array(self) - cdef double c_mean_value(self) - cdef double c_variance(self) - cdef double c_std_dev(self) - cdef object c_get_as_numpy_array(self) + cpdef void add_value(self, float val) + cpdef void increment_index(self) + cpdef double get_last_value(self) + cpdef bint is_full(self) + cpdef double mean_value(self) + cpdef double variance(self) + cpdef double std_dev(self) + cpdef np.ndarray[np.double_t, ndim=1] get_as_numpy_array(self) diff --git a/hummingbot/strategy/utils/ring_buffer.pyx b/hummingbot/strategy/utils/ring_buffer.pyx index 17a7f10954..88ef1e2df7 100644 --- a/hummingbot/strategy/utils/ring_buffer.pyx +++ b/hummingbot/strategy/utils/ring_buffer.pyx @@ -15,53 +15,54 @@ cdef class RingBuffer: def __cinit__(self, int length): self._length = length - self._buffer = np.zeros(length) + self._buffer = np.zeros(length, dtype=np.double) self._start_index = 0 self._stop_index = 0 self._is_full = False - cdef c_add_value(self, float val): + cpdef void add_value(self, float val): self._buffer[self._stop_index] = val - self.c_increment_index() + self.increment_index() - cdef c_increment_index(self): + cpdef void increment_index(self): self._stop_index = (self._stop_index + 1) % self._length if(self._start_index == self._stop_index): self._is_full = True self._start_index = (self._start_index + 1) % self._length - cdef double c_get_last_value(self): + cpdef double get_last_value(self): if self._stop_index==0: - return self.c_get_array()[-1] + return self._buffer[-1] else: - return self.c_get_array()[self._stop_index-1] + return self._buffer[self._stop_index-1] - cdef bint c_is_full(self): + cpdef bint is_full(self): return self._is_full - cdef object c_get_array(self): - return self._buffer - - cdef double c_mean_value(self): + cpdef double mean_value(self): result = np.nan if self._is_full: - result=np.mean(self.c_get_as_numpy_array()) + result=np.mean(self.get_as_numpy_array()) return result - cdef double c_variance(self): + cpdef double variance(self): result = np.nan if self._is_full: - result = np.var(self.c_get_as_numpy_array()) + result = np.var(self.get_as_numpy_array()) return result - cdef double c_std_dev(self): + cpdef double std_dev(self): result = np.nan if self._is_full: - result = np.std(self.c_get_as_numpy_array()) + result = np.std(self.get_as_numpy_array()) return result - cdef object c_get_as_numpy_array(self): - indexes = np.arange(self._start_index, stop=self._start_index + self._length) % self._length + cpdef np.ndarray[np.double_t, ndim=1] get_as_numpy_array(self): + cdef np.ndarray[np.int16_t, ndim=1] indexes + if not self._is_full: - indexes = np.arange(self._start_index, stop=self._stop_index) - return np.asarray(self.c_get_array())[indexes] + indexes = np.arange(self._start_index, stop=self._stop_index, dtype=np.int16) + else: + indexes = np.arange(self._start_index, stop=self._start_index + self._length, + dtype=np.int16) % self._length + return np.asarray(self._buffer)[indexes] From f982430aeefe477df4bdfe5f41de54792b8dca82 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 15:26:24 -0300 Subject: [PATCH 08/77] Added target_inv_stocks to debugging messages and csv dump --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 16d3a95702..b145da43d6 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -557,6 +557,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.get_last_value()) / self._spreads.get_last_value() * 100.0}% | " \ f"q={market.c_get_available_balance(self.base_asset)} | " \ + f"target_inv={self.c_calculate_target_inventory()} | " \ f"(T-t)={self._time_left/self._closing_time}" if not os.path.exists(self._csv_path): df_header = pd.DataFrame([('mid_price', @@ -564,6 +565,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'reserved_price', 'optimal_spread', 'q', + 'target_inv_stocks' 'time_left_fraction', 'mid_price std_dev', 'gamma', @@ -574,6 +576,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._reserved_price, self._optimal_spread, market.c_get_available_balance(self.base_asset), + self.c_calculate_target_inventory(), self._time_left/self._closing_time, self._mid_prices.std_dev(), self._gamma, From a0a659bd500c40c212c4e71400ed473c1138a24a Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 15:57:47 -0300 Subject: [PATCH 09/77] changed debug message and time_left restart whenever strategy starts --- .../pure_market_making_as/pure_market_making_as.pyx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index b145da43d6..c282258bde 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -524,6 +524,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): # make restored order hanging orders for order_id in restored_order_ids: self._hanging_order_ids.append(order_id) + self._time_left = self._closing_time cdef c_tick(self, double timestamp): StrategyBase.c_tick(self, timestamp) @@ -554,8 +555,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): algo_inform_text = "Algorithm not ready" if self.c_is_algorithm_ready(): self.c_calculate_reserved_price_and_optimal_spread() - algo_inform_text = f"delta(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ - f"delta(spread,opt_spread)={(self._optimal_spread - self._spreads.get_last_value()) / self._spreads.get_last_value() * 100.0}% | " \ + best_ask=self._mid_prices.get_last_value()+self._spreads.get_last_value()/2.0 + new_ask=(self._reserved_price + self._optimal_spread/2.0) + best_bid = self._mid_prices.get_last_value() - self._spreads.get_last_value() / 2.0 + new_bid = (self._reserved_price - self._optimal_spread / 2.0) + algo_inform_text = f"diff(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ + f"spread(bid,best_bid)={(new_bid-best_bid)/best_bid * 100.0}% | " \ + f"spread(ask,best_ask)={(new_ask - best_ask) / best_ask * 100.0}% | " \ f"q={market.c_get_available_balance(self.base_asset)} | " \ f"target_inv={self.c_calculate_target_inventory()} | " \ f"(T-t)={self._time_left/self._closing_time}" From 2a75860f5868234d550c0c1174347af4750af39b Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 16:16:49 -0300 Subject: [PATCH 10/77] Fixed proposal creation --- .../pure_market_making_as.pyx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index c282258bde..a3b481c020 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -646,7 +646,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self.c_is_algorithm_ready(): mid_price = self._mid_prices.get_last_value() - q = float(market.c_get_available_balance(self.base_asset)) + q = float(market.c_get_available_balance(self.base_asset)) # Should this be adjusted?? mid_price_variance = self._mid_prices.variance() self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) @@ -691,19 +691,15 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.logger().info(f"delta_quantity:{delta_quantity}") - if delta_quantity > 0: - price = self._reserved_price - self._optimal_spread / 2 - price = market.c_quantize_order_price(self.trading_pair, price) - size = market.c_quantize_order_amount(self.trading_pair, delta_quantity) - if size > 0: - buys.append(PriceSize(price, size)) - - if delta_quantity < 0: - price = self._reserved_price + self._optimal_spread / 2 - price = market.c_quantize_order_price(self.trading_pair, price) - size = market.c_quantize_order_amount(self.trading_pair, -delta_quantity) - if size>0: - sells.append(PriceSize(price, size)) + price = self._reserved_price - self._optimal_spread / 2 + price = market.c_quantize_order_price(self.trading_pair, price) + size = market.c_quantize_order_amount(self.trading_pair, abs(delta_quantity)) + buys.append(PriceSize(price, size)) + + price = self._reserved_price + self._optimal_spread / 2 + price = market.c_quantize_order_price(self.trading_pair, price) + size = market.c_quantize_order_amount(self.trading_pair, abs(delta_quantity)) + sells.append(PriceSize(price, size)) return Proposal(buys, sells) From 550b6f18abca0d05ec183729be2b3f9e7566c17a Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 16:50:41 -0300 Subject: [PATCH 11/77] Fixed bug in the q calculated --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index a3b481c020..d64471a129 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -646,7 +646,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self.c_is_algorithm_ready(): mid_price = self._mid_prices.get_last_value() - q = float(market.c_get_available_balance(self.base_asset)) # Should this be adjusted?? + q = float(market.c_get_available_balance(self.base_asset)) - self.c_calculate_target_inventory() mid_price_variance = self._mid_prices.variance() self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) From 4211291c48d39fd465eabaa88c8c2a45512ada47 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 18:09:11 -0300 Subject: [PATCH 12/77] Fixed bug in quantization of order amount --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index d64471a129..74f205252f 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -571,7 +571,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'reserved_price', 'optimal_spread', 'q', - 'target_inv_stocks' + 'target_inv_stocks', 'time_left_fraction', 'mid_price std_dev', 'gamma', @@ -673,7 +673,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): base_value = float(base_asset_amount) * mid_price inventory_value = base_value + float(quote_asset_amount) target_inventory_value = inventory_value * float(self._inventory_target_base_pct) - N = market.c_quantize_order_amount(trading_pair, target_inventory_value / mid_price) + N = market.c_quantize_order_amount(trading_pair, Decimal(str(target_inventory_value / mid_price))) return N From 82d54a7dd01801585e97322e9397a001e9702a4d Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 19:24:18 -0300 Subject: [PATCH 13/77] Removed lower limit = order_amount for q --- .../pure_market_making_as.pyx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 74f205252f..a798149d24 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -590,8 +590,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): df.to_csv(self._csv_path, mode='a', header=False, index=False) proposal = None - asset_mid_price = Decimal("0") - # asset_mid_price = self.c_set_mid_price(market_info) if self._create_timestamp <= self._current_timestamp: # 1. Create base order proposals proposal = self.c_create_base_proposal() @@ -652,7 +650,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + np.log(1 + self._gamma / self._kappa) self._optimal_ask = self._reserved_price + self._optimal_spread / 2 - self._optimal_bid = self._reserved_price + self._optimal_spread / 2 + self._optimal_bid = self._reserved_price - self._optimal_spread / 2 cdef object c_calculate_target_inventory(self): cdef: @@ -691,14 +689,12 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.logger().info(f"delta_quantity:{delta_quantity}") - price = self._reserved_price - self._optimal_spread / 2 - price = market.c_quantize_order_price(self.trading_pair, price) - size = market.c_quantize_order_amount(self.trading_pair, abs(delta_quantity)) + price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_bid))) + size = market.c_quantize_order_amount(self.trading_pair, Decimal(str(abs(delta_quantity)))) buys.append(PriceSize(price, size)) - price = self._reserved_price + self._optimal_spread / 2 - price = market.c_quantize_order_price(self.trading_pair, price) - size = market.c_quantize_order_amount(self.trading_pair, abs(delta_quantity)) + price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_ask))) + size = market.c_quantize_order_amount(self.trading_pair, Decimal(str(abs(delta_quantity)))) sells.append(PriceSize(price, size)) return Proposal(buys, sells) @@ -743,13 +739,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ExchangeBase market = self._market_info.market for buy in proposal.buys: - buy.size = s_decimal_zero if buy.size < self._order_amount else self._order_amount - - proposal.buys = [o for o in proposal.buys if o.size > 0] - + buy.size = self._order_amount for sell in proposal.sells: - sell.size = s_decimal_zero if sell.size < self._order_amount else self._order_amount - + sell.size = self._order_amount + proposal.buys = [o for o in proposal.buys if o.size > 0] proposal.sells = [o for o in proposal.sells if o.size > 0] cdef c_apply_budget_constraint(self, object proposal): From d2b8fdce094565942eff248c4d6057efcdb1d7e4 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Feb 2021 21:22:00 -0300 Subject: [PATCH 14/77] Changed algorithm to use total balance instead of available balance --- .../pure_market_making_as/pure_market_making_as.pyx | 11 +++++++---- .../pure_market_making_as_config_map.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index a798149d24..47b2c40b5c 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -666,8 +666,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): mid_price = self._mid_prices.get_last_value() # Need to review this to see if adjusted quantities are required - base_asset_amount = market.c_get_available_balance(base_asset) - quote_asset_amount = market.c_get_available_balance(quote_asset) + base_asset_amount = market.get_balance(base_asset) + quote_asset_amount = market.get_balance(quote_asset) base_value = float(base_asset_amount) * mid_price inventory_value = base_value + float(quote_asset_amount) target_inventory_value = inventory_value * float(self._inventory_target_base_pct) @@ -691,11 +691,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_bid))) size = market.c_quantize_order_amount(self.trading_pair, Decimal(str(abs(delta_quantity)))) - buys.append(PriceSize(price, size)) + 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, Decimal(str(abs(delta_quantity)))) - sells.append(PriceSize(price, size)) + if size>0: + sells.append(PriceSize(price, size)) return Proposal(buys, sells) @@ -742,6 +744,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): buy.size = self._order_amount for sell in proposal.sells: sell.size = self._order_amount + proposal.buys = [o for o in proposal.buys if o.size > 0] proposal.sells = [o for o in proposal.sells if o.size > 0] diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index 7f8c720f2b..05d4a70c75 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -164,7 +164,7 @@ def exchange_on_validated(value: str): "order_amount": ConfigVar(key="order_amount", prompt=order_amount_prompt, - required_if=lambda: pure_market_making_as_config_map.get("fixed_order_amount").value == "True", + required_if=lambda: pure_market_making_as_config_map.get("fixed_order_amount").value == "True" and pure_market_making_as_config_map.get("order_amount").value is None, type_str="decimal", validator=validate_order_amount, prompt_on_new=True), From cab8eba615636cb2c6a5e75bb37dc97695ac9e6a Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Fri, 19 Feb 2021 13:53:56 -0300 Subject: [PATCH 15/77] fix when target_inv reached so strategy keeps sending orders with order_amount --- .../pure_market_making_as/pure_market_making_as.pyx | 10 ++++++---- .../pure_market_making_as_config_map.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 47b2c40b5c..d499b93e84 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -562,7 +562,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): algo_inform_text = f"diff(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ f"spread(bid,best_bid)={(new_bid-best_bid)/best_bid * 100.0}% | " \ f"spread(ask,best_ask)={(new_ask - best_ask) / best_ask * 100.0}% | " \ - f"q={market.c_get_available_balance(self.base_asset)} | " \ + f"current_inv={market.c_get_available_balance(self.base_asset)} | " \ f"target_inv={self.c_calculate_target_inventory()} | " \ f"(T-t)={self._time_left/self._closing_time}" if not os.path.exists(self._csv_path): @@ -570,7 +570,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'spread', 'reserved_price', 'optimal_spread', - 'q', + 'current_inv', 'target_inv_stocks', 'time_left_fraction', 'mid_price std_dev', @@ -684,8 +684,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): list buys = [] list sells = [] - base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) - delta_quantity = self.c_calculate_target_inventory() - float(base_asset_amount) + delta_quantity = self._order_amount + if not self._fixed_order_amount: + base_asset_amount, _ = self.c_get_adjusted_available_balance(self.active_orders) + delta_quantity = self.c_calculate_target_inventory() - float(base_asset_amount) self.logger().info(f"delta_quantity:{delta_quantity}") diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index 05d4a70c75..b60e9bb14a 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -122,13 +122,13 @@ def exchange_on_validated(value: str): ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 10000, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 10000000, inclusive=False), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 10000, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 10000000, inclusive=False), prompt_on_new=True), "closing_time": ConfigVar(key="closing_time", From 41b7a15f2f015c7a9fccc506edabf98dc3ba1d0d Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 22 Feb 2021 03:00:20 -0300 Subject: [PATCH 16/77] Added filled_order_delay parameter --- .../pure_market_making_as.pyx | 28 +++--- .../pure_market_making_as_config_map.py | 7 ++ .../strategy/pure_market_making_as/start.py | 2 + hummingbot/strategy/utils/ring_buffer.pxd | 19 ++-- hummingbot/strategy/utils/ring_buffer.pyx | 71 ++++++++++---- ...ure_market_making_as_strategy_TEMPLATE.yml | 3 + test/strategy/test_ring_buffer.py | 97 +++++++++++++++++++ 7 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 test/strategy/test_ring_buffer.py diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index d499b93e84..64a8082bfe 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -66,6 +66,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): order_refresh_time: float = 30.0, max_order_age = 1800.0, order_refresh_tolerance_pct: Decimal = s_decimal_neg_one, + filled_order_delay: float = 60.0, inventory_target_base_pct: Decimal = s_decimal_zero, add_transaction_costs_to_orders: bool = True, asset_price_delegate: AssetPriceDelegate = None, @@ -91,6 +92,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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._asset_price_delegate = asset_price_delegate @@ -555,13 +557,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): algo_inform_text = "Algorithm not ready" if self.c_is_algorithm_ready(): self.c_calculate_reserved_price_and_optimal_spread() - best_ask=self._mid_prices.get_last_value()+self._spreads.get_last_value()/2.0 + best_ask=self._mid_prices.c_get_last_value()+self._spreads.get_last_value()/2.0 new_ask=(self._reserved_price + self._optimal_spread/2.0) - best_bid = self._mid_prices.get_last_value() - self._spreads.get_last_value() / 2.0 + best_bid = self._mid_prices.c_get_last_value() - self._spreads.get_last_value() / 2.0 new_bid = (self._reserved_price - self._optimal_spread / 2.0) - algo_inform_text = f"diff(mid,r)={(self._reserved_price - self._mid_prices.get_last_value()) / self._mid_prices.get_last_value() * 100.0}% | " \ - f"spread(bid,best_bid)={(new_bid-best_bid)/best_bid * 100.0}% | " \ - f"spread(ask,best_ask)={(new_ask - best_ask) / best_ask * 100.0}% | " \ + algo_inform_text = f"(r,mid)=({self._mid_prices.c_get_last_value()}, {self._reserved_price}) | " \ + f"(optimal_bid, best_bid)=({new_bid}, {best_bid}) | " \ + f"(optimal_ask, best_ask)=({new_ask}, {best_ask}) | " \ f"current_inv={market.c_get_available_balance(self.base_asset)} | " \ f"target_inv={self.c_calculate_target_inventory()} | " \ f"(T-t)={self._time_left/self._closing_time}" @@ -571,20 +573,20 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'reserved_price', 'optimal_spread', 'current_inv', - 'target_inv_stocks', + 'target_inv', 'time_left_fraction', 'mid_price std_dev', 'gamma', 'kappa')]) df_header.to_csv(self._csv_path, mode='a', header=False, index=False) - df = pd.DataFrame([(self._mid_prices.get_last_value(), + df = pd.DataFrame([(self._mid_prices.c_get_last_value(), self._spreads.get_last_value(), self._reserved_price, self._optimal_spread, market.c_get_available_balance(self.base_asset), self.c_calculate_target_inventory(), self._time_left/self._closing_time, - self._mid_prices.std_dev(), + self._mid_prices.c_std_dev(), self._gamma, self._kappa)]) df.to_csv(self._csv_path, mode='a', header=False, index=False) @@ -621,7 +623,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.logger().info("Recycling algorithm time left...") cdef c_save_mid_price(self): - self._mid_prices.add_value(self.c_get_mid_price()) + self._mid_prices.c_add_value(self.c_get_mid_price()) cdef c_save_spread(self): self._spreads.add_value(self.c_get_spread()) @@ -643,9 +645,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double buy_fee if self.c_is_algorithm_ready(): - mid_price = self._mid_prices.get_last_value() + mid_price = self._mid_prices.c_get_last_value() q = float(market.c_get_available_balance(self.base_asset)) - self.c_calculate_target_inventory() - mid_price_variance = self._mid_prices.variance() + mid_price_variance = self._mid_prices.c_variance() self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + np.log(1 + self._gamma / self._kappa) @@ -664,7 +666,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): double target_inventory_value double N - mid_price = self._mid_prices.get_last_value() + mid_price = self._mid_prices.c_get_last_value() # Need to review this to see if adjusted quantities are required base_asset_amount = market.get_balance(base_asset) quote_asset_amount = market.get_balance(quote_asset) @@ -676,7 +678,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return N cdef bint c_is_algorithm_ready(self): - return self._mid_prices.is_full() + return self._mid_prices.c_is_full() cdef object c_create_base_proposal(self): cdef: diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index b60e9bb14a..c9802db71c 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -175,6 +175,13 @@ def exchange_on_validated(value: str): 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 your target base asset percentage? Enter 50 for 50% >>> ", diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index 327be7de6d..cc027e82ec 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -29,6 +29,7 @@ def start(self): price_source_exchange = c_map.get("price_source_exchange").value price_source_market = c_map.get("price_source_market").value price_source_custom_api = c_map.get("price_source_custom_api").value + filled_order_delay = c_map.get("filled_order_delay").value order_refresh_tolerance_pct = c_map.get("order_refresh_tolerance_pct").value / Decimal('100') trading_pair: str = raw_trading_pair @@ -61,6 +62,7 @@ def start(self): 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=True, logging_options=strategy_logging_options, asset_price_delegate=asset_price_delegate, diff --git a/hummingbot/strategy/utils/ring_buffer.pxd b/hummingbot/strategy/utils/ring_buffer.pxd index 2bbc1da4ea..72c006bdb5 100644 --- a/hummingbot/strategy/utils/ring_buffer.pxd +++ b/hummingbot/strategy/utils/ring_buffer.pxd @@ -4,17 +4,18 @@ cimport numpy as np cdef class RingBuffer: cdef: - np.double_t[:] _buffer + np.float64_t[:] _buffer int64_t _start_index int64_t _stop_index int64_t _length bint _is_full - cpdef void add_value(self, float val) - cpdef void increment_index(self) - cpdef double get_last_value(self) - cpdef bint is_full(self) - cpdef double mean_value(self) - cpdef double variance(self) - cpdef double std_dev(self) - cpdef np.ndarray[np.double_t, ndim=1] get_as_numpy_array(self) + cdef void c_add_value(self, float val) + cdef void c_increment_index(self) + cdef double c_get_last_value(self) + cdef bint c_is_full(self) + cdef bint c_is_empty(self) + cdef double c_mean_value(self) + cdef double c_variance(self) + cdef double c_std_dev(self) + cdef np.ndarray[np.double_t, ndim=1] c_get_as_numpy_array(self) diff --git a/hummingbot/strategy/utils/ring_buffer.pyx b/hummingbot/strategy/utils/ring_buffer.pyx index 88ef1e2df7..1cf98f6350 100644 --- a/hummingbot/strategy/utils/ring_buffer.pyx +++ b/hummingbot/strategy/utils/ring_buffer.pyx @@ -15,49 +15,54 @@ cdef class RingBuffer: def __cinit__(self, int length): self._length = length - self._buffer = np.zeros(length, dtype=np.double) + self._buffer = np.zeros(length, dtype=np.float64) self._start_index = 0 self._stop_index = 0 self._is_full = False - cpdef void add_value(self, float val): + def __dealloc__(self): + self._buffer = None + + cdef void c_add_value(self, float val): self._buffer[self._stop_index] = val - self.increment_index() + self.c_increment_index() - cpdef void increment_index(self): + cdef void c_increment_index(self): self._stop_index = (self._stop_index + 1) % self._length if(self._start_index == self._stop_index): self._is_full = True self._start_index = (self._start_index + 1) % self._length - cpdef double get_last_value(self): - if self._stop_index==0: - return self._buffer[-1] - else: - return self._buffer[self._stop_index-1] + cdef bint c_is_empty(self): + return (not self._is_full) and (self._start_index==self._stop_index) - cpdef bint is_full(self): + cdef double c_get_last_value(self): + if self.c_is_empty(): + return np.nan + return self._buffer[self._stop_index-1] + + cdef bint c_is_full(self): return self._is_full - cpdef double mean_value(self): + cdef double c_mean_value(self): result = np.nan if self._is_full: - result=np.mean(self.get_as_numpy_array()) + result=np.mean(self.c_get_as_numpy_array()) return result - cpdef double variance(self): + cdef double c_variance(self): result = np.nan if self._is_full: - result = np.var(self.get_as_numpy_array()) + result = np.var(self.c_get_as_numpy_array()) return result - cpdef double std_dev(self): + cdef double c_std_dev(self): result = np.nan if self._is_full: - result = np.std(self.get_as_numpy_array()) + result = np.std(self.c_get_as_numpy_array()) return result - cpdef np.ndarray[np.double_t, ndim=1] get_as_numpy_array(self): + cdef np.ndarray[np.double_t, ndim=1] c_get_as_numpy_array(self): cdef np.ndarray[np.int16_t, ndim=1] indexes if not self._is_full: @@ -66,3 +71,35 @@ cdef class RingBuffer: indexes = np.arange(self._start_index, stop=self._start_index + self._length, dtype=np.int16) % self._length return np.asarray(self._buffer)[indexes] + + def __init__(self, length): + self._length = length + self._buffer = np.zeros(length, dtype=np.double) + self._start_index = 0 + self._stop_index = 0 + self._is_full = False + + def add_value(self, val): + self.c_add_value(val) + + def get_as_numpy_array(self): + return self.c_get_as_numpy_array() + + def get_last_value(self): + return self.c_get_last_value() + + @property + def is_full(self): + return self.c_is_full() + + @property + def mean_value(self): + return self.c_mean_value() + + @property + def std_dev(self): + return self.c_std_dev() + + @property + def variance(self): + return self.c_variance() diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 12f6ae34f1..30280193ce 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -28,6 +28,9 @@ order_amount: null # Size of your bid and ask order. fixed_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 diff --git a/test/strategy/test_ring_buffer.py b/test/strategy/test_ring_buffer.py new file mode 100644 index 0000000000..907296fbcb --- /dev/null +++ b/test/strategy/test_ring_buffer.py @@ -0,0 +1,97 @@ +import unittest +from hummingbot.strategy.utils.ring_buffer import RingBuffer +import numpy as np +from decimal import Decimal + + +class RingBufferTest(unittest.TestCase): + BUFFER_LENGTH = 30 + + def setUp(self) -> None: + self.buffer = RingBuffer(self.BUFFER_LENGTH) + + def fill_buffer_with_zeros(self): + for i in range(self.BUFFER_LENGTH): + self.buffer.add_value(0) + + def test_add_value(self): + self.buffer.add_value(1) + self.assertEqual(self.buffer.get_as_numpy_array().size, 1) + + def test_is_full(self): + self.assertFalse(self.buffer.is_full) # Current occupation = 0 + self.buffer.add_value(1) + self.assertFalse(self.buffer.is_full) # Current occupation = 1 + for i in range(self.BUFFER_LENGTH - 2): + self.buffer.add_value(i) + self.assertFalse(self.buffer.is_full) # Current occupation = BUFFER_LENGTH-1 + self.buffer.add_value(1) + self.assertTrue(self.buffer.is_full) # Current occupation = BUFFER_LENGTH + + def test_add_when_full(self): + for i in range(self.BUFFER_LENGTH): + self.buffer.add_value(1) + self.assertTrue(self.buffer.is_full) + # Filled with ones, total sum equals BUFFER_LENGTH + self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), self.BUFFER_LENGTH) + # Add zeros till length/2 check total sum has decreased accordingly + mid_point = self.BUFFER_LENGTH // 2 + for i in range(mid_point): + self.buffer.add_value(0) + self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), self.BUFFER_LENGTH - mid_point) + # Add remaining zeros to complete length, sum should go to zero + for i in range(self.BUFFER_LENGTH - mid_point): + self.buffer.add_value(0) + self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), 0) + + def test_mean(self): + # When not full, mean=nan + self.assertTrue(np.isnan(self.buffer.mean_value)) + for i in range(self.BUFFER_LENGTH // 2): + self.buffer.add_value(1) + # Still not full, mean=nan + self.assertTrue(np.isnan(self.buffer.mean_value)) + for i in range(self.BUFFER_LENGTH - self.BUFFER_LENGTH // 2): + self.buffer.add_value(1) + # Once full, mean != nan + self.assertEqual(self.buffer.mean_value, 1.0) + + def test_mean_with_alternated_samples(self): + for i in range(self.BUFFER_LENGTH * 3): + self.buffer.add_value(2 * ((-1) ** i)) + if self.buffer.is_full: + self.assertEqual(self.buffer.mean_value, 0) + + def test_std_dev_and_variance(self): + # When not full, stddev=var=nan + self.assertTrue(np.isnan(self.buffer.std_dev)) + self.assertTrue(np.isnan(self.buffer.variance)) + for i in range(self.BUFFER_LENGTH // 2): + self.buffer.add_value(1) + # Still not full, stddev=var=nan + self.assertTrue(np.isnan(self.buffer.std_dev)) + self.assertTrue(np.isnan(self.buffer.variance)) + for i in range(self.BUFFER_LENGTH - self.BUFFER_LENGTH // 2): + self.buffer.add_value(1) + # Once full, std_dev = variance = 0 in this case + self.assertEqual(self.buffer.std_dev, 0) + self.assertEqual(self.buffer.variance, 0) + + def test_std_dev_and_variance_with_alternated_samples(self): + for i in range(self.BUFFER_LENGTH * 3): + self.buffer.add_value(2 * ((-1)**i)) + if self.buffer.is_full: + self.assertEqual(self.buffer.std_dev, 2) + self.assertEqual(self.buffer.variance, 4) + + def test_get_last_value(self): + self.assertTrue(np.isnan(self.buffer.get_last_value())) + expected_values = [-2, -1.0, 0, 3, 1e10] + for value in expected_values: + self.buffer.add_value(value) + self.assertEqual(self.buffer.get_last_value(), value) + + # Decimals are casted when added to numpy array as np.float64. No exact match + value = Decimal(3.141592653) + self.buffer.add_value(value) + self.assertAlmostEqual(float(value), self.buffer.get_last_value(), 6) From 8d9ef8ddb816b975f53a6eb0cd3a42155b05ea80 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 22 Feb 2021 12:33:52 -0300 Subject: [PATCH 17/77] Merged development RingBuffer push --- .../pure_market_making_as.pxd | 2 +- .../pure_market_making_as.pyx | 2 +- hummingbot/strategy/utils/__init__.py | 0 hummingbot/strategy/utils/ring_buffer.pxd | 21 ---- hummingbot/strategy/utils/ring_buffer.pyx | 105 ------------------ test/strategy/test_ring_buffer.py | 97 ---------------- 6 files changed, 2 insertions(+), 225 deletions(-) delete mode 100644 hummingbot/strategy/utils/__init__.py delete mode 100644 hummingbot/strategy/utils/ring_buffer.pxd delete mode 100644 hummingbot/strategy/utils/ring_buffer.pyx delete mode 100644 test/strategy/test_ring_buffer.py diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index b8cbaf8b25..68b2467f56 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -2,7 +2,7 @@ from libc.stdint cimport int64_t from hummingbot.strategy.strategy_base cimport StrategyBase -from ..utils.ring_buffer cimport RingBuffer +from ..__utils__.ring_buffer cimport RingBuffer cdef class PureMarketMakingASStrategy(StrategyBase): diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 64a8082bfe..e2b8e0870a 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -35,7 +35,7 @@ from .pure_market_making_as_order_tracker import PureMarketMakingASOrderTracker from .asset_price_delegate cimport AssetPriceDelegate from .asset_price_delegate import AssetPriceDelegate from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate -from ..utils.ring_buffer cimport RingBuffer +from ..__utils__.ring_buffer cimport RingBuffer NaN = float("nan") diff --git a/hummingbot/strategy/utils/__init__.py b/hummingbot/strategy/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hummingbot/strategy/utils/ring_buffer.pxd b/hummingbot/strategy/utils/ring_buffer.pxd deleted file mode 100644 index 72c006bdb5..0000000000 --- a/hummingbot/strategy/utils/ring_buffer.pxd +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -from libc.stdint cimport int64_t -cimport numpy as np - -cdef class RingBuffer: - cdef: - np.float64_t[:] _buffer - int64_t _start_index - int64_t _stop_index - int64_t _length - bint _is_full - - cdef void c_add_value(self, float val) - cdef void c_increment_index(self) - cdef double c_get_last_value(self) - cdef bint c_is_full(self) - cdef bint c_is_empty(self) - cdef double c_mean_value(self) - cdef double c_variance(self) - cdef double c_std_dev(self) - cdef np.ndarray[np.double_t, ndim=1] c_get_as_numpy_array(self) diff --git a/hummingbot/strategy/utils/ring_buffer.pyx b/hummingbot/strategy/utils/ring_buffer.pyx deleted file mode 100644 index 1cf98f6350..0000000000 --- a/hummingbot/strategy/utils/ring_buffer.pyx +++ /dev/null @@ -1,105 +0,0 @@ -import numpy as np -import logging -cimport numpy as np - - -pmm_logger = None - -cdef class RingBuffer: - @classmethod - def logger(cls): - global pmm_logger - if pmm_logger is None: - pmm_logger = logging.getLogger(__name__) - return pmm_logger - - def __cinit__(self, int length): - self._length = length - self._buffer = np.zeros(length, dtype=np.float64) - self._start_index = 0 - self._stop_index = 0 - self._is_full = False - - def __dealloc__(self): - self._buffer = None - - cdef void c_add_value(self, float val): - self._buffer[self._stop_index] = val - self.c_increment_index() - - cdef void c_increment_index(self): - self._stop_index = (self._stop_index + 1) % self._length - if(self._start_index == self._stop_index): - self._is_full = True - self._start_index = (self._start_index + 1) % self._length - - cdef bint c_is_empty(self): - return (not self._is_full) and (self._start_index==self._stop_index) - - cdef double c_get_last_value(self): - if self.c_is_empty(): - return np.nan - return self._buffer[self._stop_index-1] - - cdef bint c_is_full(self): - return self._is_full - - cdef double c_mean_value(self): - result = np.nan - if self._is_full: - result=np.mean(self.c_get_as_numpy_array()) - return result - - cdef double c_variance(self): - result = np.nan - if self._is_full: - result = np.var(self.c_get_as_numpy_array()) - return result - - cdef double c_std_dev(self): - result = np.nan - if self._is_full: - result = np.std(self.c_get_as_numpy_array()) - return result - - cdef np.ndarray[np.double_t, ndim=1] c_get_as_numpy_array(self): - cdef np.ndarray[np.int16_t, ndim=1] indexes - - if not self._is_full: - indexes = np.arange(self._start_index, stop=self._stop_index, dtype=np.int16) - else: - indexes = np.arange(self._start_index, stop=self._start_index + self._length, - dtype=np.int16) % self._length - return np.asarray(self._buffer)[indexes] - - def __init__(self, length): - self._length = length - self._buffer = np.zeros(length, dtype=np.double) - self._start_index = 0 - self._stop_index = 0 - self._is_full = False - - def add_value(self, val): - self.c_add_value(val) - - def get_as_numpy_array(self): - return self.c_get_as_numpy_array() - - def get_last_value(self): - return self.c_get_last_value() - - @property - def is_full(self): - return self.c_is_full() - - @property - def mean_value(self): - return self.c_mean_value() - - @property - def std_dev(self): - return self.c_std_dev() - - @property - def variance(self): - return self.c_variance() diff --git a/test/strategy/test_ring_buffer.py b/test/strategy/test_ring_buffer.py deleted file mode 100644 index 907296fbcb..0000000000 --- a/test/strategy/test_ring_buffer.py +++ /dev/null @@ -1,97 +0,0 @@ -import unittest -from hummingbot.strategy.utils.ring_buffer import RingBuffer -import numpy as np -from decimal import Decimal - - -class RingBufferTest(unittest.TestCase): - BUFFER_LENGTH = 30 - - def setUp(self) -> None: - self.buffer = RingBuffer(self.BUFFER_LENGTH) - - def fill_buffer_with_zeros(self): - for i in range(self.BUFFER_LENGTH): - self.buffer.add_value(0) - - def test_add_value(self): - self.buffer.add_value(1) - self.assertEqual(self.buffer.get_as_numpy_array().size, 1) - - def test_is_full(self): - self.assertFalse(self.buffer.is_full) # Current occupation = 0 - self.buffer.add_value(1) - self.assertFalse(self.buffer.is_full) # Current occupation = 1 - for i in range(self.BUFFER_LENGTH - 2): - self.buffer.add_value(i) - self.assertFalse(self.buffer.is_full) # Current occupation = BUFFER_LENGTH-1 - self.buffer.add_value(1) - self.assertTrue(self.buffer.is_full) # Current occupation = BUFFER_LENGTH - - def test_add_when_full(self): - for i in range(self.BUFFER_LENGTH): - self.buffer.add_value(1) - self.assertTrue(self.buffer.is_full) - # Filled with ones, total sum equals BUFFER_LENGTH - self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), self.BUFFER_LENGTH) - # Add zeros till length/2 check total sum has decreased accordingly - mid_point = self.BUFFER_LENGTH // 2 - for i in range(mid_point): - self.buffer.add_value(0) - self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), self.BUFFER_LENGTH - mid_point) - # Add remaining zeros to complete length, sum should go to zero - for i in range(self.BUFFER_LENGTH - mid_point): - self.buffer.add_value(0) - self.assertEqual(np.sum(self.buffer.get_as_numpy_array()), 0) - - def test_mean(self): - # When not full, mean=nan - self.assertTrue(np.isnan(self.buffer.mean_value)) - for i in range(self.BUFFER_LENGTH // 2): - self.buffer.add_value(1) - # Still not full, mean=nan - self.assertTrue(np.isnan(self.buffer.mean_value)) - for i in range(self.BUFFER_LENGTH - self.BUFFER_LENGTH // 2): - self.buffer.add_value(1) - # Once full, mean != nan - self.assertEqual(self.buffer.mean_value, 1.0) - - def test_mean_with_alternated_samples(self): - for i in range(self.BUFFER_LENGTH * 3): - self.buffer.add_value(2 * ((-1) ** i)) - if self.buffer.is_full: - self.assertEqual(self.buffer.mean_value, 0) - - def test_std_dev_and_variance(self): - # When not full, stddev=var=nan - self.assertTrue(np.isnan(self.buffer.std_dev)) - self.assertTrue(np.isnan(self.buffer.variance)) - for i in range(self.BUFFER_LENGTH // 2): - self.buffer.add_value(1) - # Still not full, stddev=var=nan - self.assertTrue(np.isnan(self.buffer.std_dev)) - self.assertTrue(np.isnan(self.buffer.variance)) - for i in range(self.BUFFER_LENGTH - self.BUFFER_LENGTH // 2): - self.buffer.add_value(1) - # Once full, std_dev = variance = 0 in this case - self.assertEqual(self.buffer.std_dev, 0) - self.assertEqual(self.buffer.variance, 0) - - def test_std_dev_and_variance_with_alternated_samples(self): - for i in range(self.BUFFER_LENGTH * 3): - self.buffer.add_value(2 * ((-1)**i)) - if self.buffer.is_full: - self.assertEqual(self.buffer.std_dev, 2) - self.assertEqual(self.buffer.variance, 4) - - def test_get_last_value(self): - self.assertTrue(np.isnan(self.buffer.get_last_value())) - expected_values = [-2, -1.0, 0, 3, 1e10] - for value in expected_values: - self.buffer.add_value(value) - self.assertEqual(self.buffer.get_last_value(), value) - - # Decimals are casted when added to numpy array as np.float64. No exact match - value = Decimal(3.141592653) - self.buffer.add_value(value) - self.assertAlmostEqual(float(value), self.buffer.get_last_value(), 6) From b08bd65a8d445ec02a4b7ac82ab74cb223e623b4 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 22 Feb 2021 21:03:36 -0300 Subject: [PATCH 18/77] Incrementing range for gamma and kappa --- .../pure_market_making_as/pure_market_making_as_config_map.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index c9802db71c..0342dba693 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -122,13 +122,13 @@ def exchange_on_validated(value: str): ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 10000000, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", type_str="float", - validator=lambda v: validate_decimal(v, 0, 10000000, inclusive=False), + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "closing_time": ConfigVar(key="closing_time", From 220e394af624f2144c579d4dc6c07fef387daa0d Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 4 Mar 2021 11:22:15 -0300 Subject: [PATCH 19/77] Adding trailing indicators and tweaks to the strategy --- .../trailing_indicators/average_volatility.py | 14 + .../base_trailing_indicator.py | 49 ++++ .../exponential_moving_average.py | 17 ++ .../pure_market_making_as.pxd | 52 ++-- .../pure_market_making_as.pyx | 277 ++++++++---------- .../pure_market_making_as_config_map.py | 89 ++++-- .../strategy/pure_market_making_as/start.py | 23 +- ...ure_market_making_as_strategy_TEMPLATE.yml | 11 +- 8 files changed, 307 insertions(+), 225 deletions(-) create mode 100644 hummingbot/strategy/__utils__/trailing_indicators/average_volatility.py create mode 100644 hummingbot/strategy/__utils__/trailing_indicators/base_trailing_indicator.py create mode 100644 hummingbot/strategy/__utils__/trailing_indicators/exponential_moving_average.py 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/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index 68b2467f56..597dea7f5b 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -2,41 +2,26 @@ from libc.stdint cimport int64_t from hummingbot.strategy.strategy_base cimport StrategyBase -from ..__utils__.ring_buffer cimport RingBuffer +from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator cdef class PureMarketMakingASStrategy(StrategyBase): cdef: object _market_info - - object _bid_spread - object _ask_spread object _minimum_spread object _order_amount - bint _fixed_order_amount - int _order_levels - int _buy_levels - int _sell_levels - object _order_level_spread - object _order_level_amount double _order_refresh_time double _max_order_age object _order_refresh_tolerance_pct double _filled_order_delay - bint _inventory_skew_enabled object _inventory_target_base_pct - object _inventory_range_multiplier bint _hanging_orders_enabled object _hanging_orders_cancel_pct bint _order_optimization_enabled - object _ask_order_optimization_depth - object _bid_order_optimization_depth bint _add_transaction_costs_to_orders object _asset_price_delegate object _inventory_cost_price_delegate object _price_type - object _price_ceiling - object _price_floor bint _hb_app_notification double _cancel_timestamp @@ -51,28 +36,31 @@ cdef class PureMarketMakingASStrategy(StrategyBase): int64_t _logging_options object _last_own_trade_price list _hanging_aged_order_prices - double _kappa - double _gamma - double _closing_time - double _time_left - double _reserved_price - double _optimal_spread - double _optimal_bid - double _optimal_ask - RingBuffer _mid_prices - RingBuffer _spreads + int _buffer_sampling_period + double _last_sampling_timestamp + bint _parameters_based_on_spread + object _min_spread + object _max_spread + object _kappa + object _gamma + object _eta + object _closing_time + object _time_left + object _reserved_price + object _optimal_spread + object _optimal_bid + object _optimal_ask + double _latest_parameter_calculation_vol str _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_levels_modifiers(self, object proposal) - cdef c_apply_price_band(self, object proposal) cdef c_apply_order_price_modifiers(self, object proposal) - cdef c_apply_order_amount_constraint(self, object proposal) + cdef c_apply_order_amount_modifiers(self, object proposal) cdef c_apply_budget_constraint(self, object proposal) - cdef c_filter_out_takers(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) @@ -82,10 +70,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef bint c_to_create_orders(self, object proposal) cdef c_execute_orders_proposal(self, object proposal) cdef set_timers(self) - cdef c_save_mid_price(self) cdef double c_get_spread(self) - cdef c_save_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) + cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index e2b8e0870a..41947b581f 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -35,7 +35,7 @@ from .pure_market_making_as_order_tracker import PureMarketMakingASOrderTracker from .asset_price_delegate cimport AssetPriceDelegate from .asset_price_delegate import AssetPriceDelegate from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate -from ..__utils__.ring_buffer cimport RingBuffer +from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator NaN = float("nan") @@ -66,29 +66,31 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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, asset_price_delegate: AssetPriceDelegate = None, price_type: str = "mid_price", - price_ceiling: Decimal = s_decimal_neg_one, - price_floor: Decimal = s_decimal_neg_one, logging_options: int = OPTION_LOG_ALL, status_report_interval: float = 900, hb_app_notification: bool = False, - kappa: float = 0.1, - gamma: float = 0.5, - closing_time: float = 3600.0 * 24 * 1e3, - fixed_order_amount: bool = False, + parameters_based_on_spread: bool = True, + min_spread: Decimal = Decimal("0.15"), + max_spread: Decimal = Decimal("2"), + kappa: Decimal = Decimal("0.1"), + gamma: Decimal = Decimal("0.5"), + eta: Decimal = Decimal("0.005"), + closing_time: Decimal = Decimal("86400000"), data_path: str = '', buffer_size: int = 30, + buffer_sampling_period: int = 60 ): super().__init__() self._sb_order_tracker = PureMarketMakingASOrderTracker() self._market_info = market_info self._order_amount = order_amount - self._fixed_order_amount = fixed_order_amount - self._order_level_spread = 0 + 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 @@ -97,8 +99,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._add_transaction_costs_to_orders = add_transaction_costs_to_orders self._asset_price_delegate = asset_price_delegate self._price_type = self.get_price_type(price_type) - self._price_ceiling = price_ceiling - self._price_floor = price_floor self._hb_app_notification = hb_app_notification self._cancel_timestamp = 0 @@ -115,16 +115,22 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) - self._mid_prices = RingBuffer(buffer_size) - self._spreads = RingBuffer(buffer_size) + self._parameters_based_on_spread = parameters_based_on_spread + self._min_spread = min_spread + self._max_spread = max_spread + self._avg_vol=AverageVolatilityIndicator(buffer_size, buffer_size) + self._buffer_sampling_period = buffer_sampling_period + self._last_sampling_timestamp = 0 self._kappa = kappa self._gamma = gamma + self._eta = eta self._time_left = closing_time self._closing_time = closing_time - self._reserved_price = 0 - self._optimal_spread = 0 - self._optimal_ask = 0 - self._optimal_bid = 0 + self._latest_parameter_calculation_vol = 0 + 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._csv_path = os.path.join(data_path, f"PMM_AS_{pd.Timestamp.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv") try: @@ -189,14 +195,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def order_level_amount(self, value: Decimal): self._order_level_amount = value - @property - def order_level_spread(self) -> Decimal: - return self._order_level_spread - - @order_level_spread.setter - def order_level_spread(self, value: Decimal): - self._order_level_spread = value - @property def inventory_target_base_pct(self) -> Decimal: return self._inventory_target_base_pct @@ -205,14 +203,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def inventory_target_base_pct(self, value: Decimal): self._inventory_target_base_pct = value - @property - def inventory_range_multiplier(self) -> Decimal: - return self._inventory_range_multiplier - - @inventory_range_multiplier.setter - def inventory_range_multiplier(self, value: Decimal): - self._inventory_range_multiplier = value - @property def hanging_orders_enabled(self) -> bool: return self._hanging_orders_enabled @@ -269,22 +259,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def add_transaction_costs_to_orders(self, value: bool): self._add_transaction_costs_to_orders = value - @property - def price_ceiling(self) -> Decimal: - return self._price_ceiling - - @price_ceiling.setter - def price_ceiling(self, value: Decimal): - self._price_ceiling = value - - @property - def price_floor(self) -> Decimal: - return self._price_floor - - @price_floor.setter - def price_floor(self, value: Decimal): - self._price_floor = value - @property def base_asset(self): return self._market_info.base_asset @@ -430,7 +404,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): age = pd.Timestamp(int(time.time()) - int(order.client_order_id[-16:])/1e6, unit='s').strftime('%H:%M:%S') - amount_orig = np.abs(self.c_calculate_target_inventory() - float(market.get_balance(base_asset))) + amount_orig = self._order_amount data.append([ "", "buy" if order.is_buy else "sell", @@ -448,6 +422,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): markets_columns = ["Exchange", "Market", "Best Bid", "Best Ask", f"Ref Price ({self._price_type.name})"] if self._price_type is PriceType.LastOwnTrade and self._last_own_trade_price.is_nan(): markets_columns[-1] = "Ref Price (MidPrice)" + markets_columns.append('Reserved Price') market_books = [(self._market_info.market, self._market_info.trading_pair)] if type(self._asset_price_delegate) is OrderBookAssetPriceDelegate: market_books.append((self._asset_price_delegate.market, self._asset_price_delegate.trading_pair)) @@ -473,7 +448,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): trading_pair, float(bid_price), float(ask_price), - float(ref_price) + float(ref_price), + round(self._reserved_price, 5), ]) return pd.DataFrame(data=markets_data, columns=markets_columns).replace(np.nan, '', regex=True) @@ -501,6 +477,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): else: lines.extend(["", " No active maker orders."]) + volatility_pct = self._avg_vol.current_value / float(self.c_get_mid_price()) * 100.0 + lines.extend(["", f"Avellaneda-Stoikov: Gamma= {self._gamma:.5E} | Kappa= {self._kappa:.5E} | Volatility= {volatility_pct:.3f}%"]) + warning_lines.extend(self.balance_warning([self._market_info])) if len(warning_lines) > 0: @@ -554,19 +533,18 @@ cdef class PureMarketMakingASStrategy(StrategyBase): f"making may be dangerous when markets or networks are unstable.") self.c_collect_market_variables(timestamp) - algo_inform_text = "Algorithm not ready" if self.c_is_algorithm_ready(): + if (self._gamma == s_decimal_neg_one or self._kappa == s_decimal_neg_one) or \ + (self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > 0.3): + self.c_recalculate_parameters() self.c_calculate_reserved_price_and_optimal_spread() - best_ask=self._mid_prices.c_get_last_value()+self._spreads.get_last_value()/2.0 - new_ask=(self._reserved_price + self._optimal_spread/2.0) - best_bid = self._mid_prices.c_get_last_value() - self._spreads.get_last_value() / 2.0 - new_bid = (self._reserved_price - self._optimal_spread / 2.0) - algo_inform_text = f"(r,mid)=({self._mid_prices.c_get_last_value()}, {self._reserved_price}) | " \ - f"(optimal_bid, best_bid)=({new_bid}, {best_bid}) | " \ - f"(optimal_ask, best_ask)=({new_ask}, {best_ask}) | " \ - f"current_inv={market.c_get_available_balance(self.base_asset)} | " \ - f"target_inv={self.c_calculate_target_inventory()} | " \ - f"(T-t)={self._time_left/self._closing_time}" + mid_price = self.c_get_mid_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._csv_path): df_header = pd.DataFrame([('mid_price', 'spread', @@ -579,14 +557,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'gamma', 'kappa')]) df_header.to_csv(self._csv_path, mode='a', header=False, index=False) - df = pd.DataFrame([(self._mid_prices.c_get_last_value(), - self._spreads.get_last_value(), + df = pd.DataFrame([(mid_price, + spread, self._reserved_price, self._optimal_spread, market.c_get_available_balance(self.base_asset), self.c_calculate_target_inventory(), self._time_left/self._closing_time, - self._mid_prices.c_std_dev(), + self._avg_vol.current_value, self._gamma, self._kappa)]) df.to_csv(self._csv_path, mode='a', header=False, index=False) @@ -595,11 +573,11 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self._create_timestamp <= self._current_timestamp: # 1. Create base order proposals proposal = self.c_create_base_proposal() - # 2. Apply functions that limit numbers of buys and sells proposal - self.c_apply_order_levels_modifiers(proposal) + # 2. Apply functions that modify orders amount + self.c_apply_order_amount_modifiers(proposal) # 3. Apply functions that modify orders price self.c_apply_order_price_modifiers(proposal) - # 5. Apply budget constraint, i.e. can't buy/sell more than what you have. + # 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) @@ -610,23 +588,25 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_execute_orders_proposal(refresh_proposal) if self.c_to_create_orders(proposal): self.c_execute_orders_proposal(proposal) - self.logger().info(algo_inform_text) finally: self._last_timestamp = timestamp cdef c_collect_market_variables(self, double timestamp): - self.c_save_mid_price() - self.c_save_spread() - self._time_left = max(self._time_left - (timestamp-self._last_timestamp)*1e3, 0) + if timestamp - self._last_sampling_timestamp >= self._buffer_sampling_period: + self._avg_vol.add_sample(self.c_get_mid_price()) + self._last_sampling_timestamp = timestamp + self._time_left = max(self._time_left - Decimal(timestamp - self._last_timestamp) * 1000, 0) if self._time_left == 0: - self._time_left = self._closing_time # Re-cycle algorithm - self.logger().info("Recycling algorithm time left...") - - cdef c_save_mid_price(self): - self._mid_prices.c_add_value(self.c_get_mid_price()) + # 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.") - cdef c_save_spread(self): - self._spreads.add_value(self.c_get_spread()) + cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol): + if self._latest_parameter_calculation_vol == 0: + return 0 + return abs(self._latest_parameter_calculation_vol - current_vol) / self._latest_parameter_calculation_vol cdef double c_get_spread(self): cdef: @@ -638,21 +618,23 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef c_calculate_reserved_price_and_optimal_spread(self): cdef: ExchangeBase market = self._market_info.market - double mid_price - double base_balance - double mid_price_variance - double time_left_fraction = self._time_left / self._closing_time - double buy_fee + + time_left_fraction = Decimal(str(self._time_left / self._closing_time)) if self.c_is_algorithm_ready(): - mid_price = self._mid_prices.c_get_last_value() - q = float(market.c_get_available_balance(self.base_asset)) - self.c_calculate_target_inventory() - mid_price_variance = self._mid_prices.c_variance() - self._reserved_price=mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + mid_price = self.c_get_mid_price() + q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) + mid_price_variance = Decimal(str(self._avg_vol.current_value)) ** 2 + self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + Decimal(1 + self._gamma / self._kappa).ln() + self._optimal_ask = min(self._reserved_price + self._optimal_spread / 2, mid_price * (Decimal(1) + self._max_spread)) + self._optimal_bid = max(self._reserved_price - self._optimal_spread / 2, mid_price * (Decimal(1) - self._max_spread)) - self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + np.log(1 + self._gamma / self._kappa) - self._optimal_ask = self._reserved_price + self._optimal_spread / 2 - self._optimal_bid = self._reserved_price - self._optimal_spread / 2 + self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " + f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " + f"q={q:.4f} | " + f"sigma2={mid_price_variance:.4f}") cdef object c_calculate_target_inventory(self): cdef: @@ -660,25 +642,37 @@ cdef class PureMarketMakingASStrategy(StrategyBase): str trading_pair = self._market_info.trading_pair str base_asset = self._market_info.base_asset str quote_asset = self._market_info.quote_asset - double mid_price - double base_value - double inventory_value - double target_inventory_value - double N + object mid_price + object base_value + object inventory_value + object target_inventory_value - mid_price = self._mid_prices.c_get_last_value() + mid_price = self.c_get_mid_price() # Need to review this to see if adjusted quantities are required base_asset_amount = market.get_balance(base_asset) quote_asset_amount = market.get_balance(quote_asset) - base_value = float(base_asset_amount) * mid_price - inventory_value = base_value + float(quote_asset_amount) - target_inventory_value = inventory_value * float(self._inventory_target_base_pct) - N = market.c_quantize_order_amount(trading_pair, Decimal(str(target_inventory_value / mid_price))) + base_value = base_asset_amount * mid_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 / mid_price))) - return N + cdef c_recalculate_parameters(self): + cdef: + ExchangeBase market = self._market_info.market + + q = market.c_get_available_balance(self.base_asset) - self.c_calculate_target_inventory() + min_spread = self._min_spread * self.c_get_mid_price() + max_spread = self._max_spread * self.c_get_mid_price() + vol = Decimal(str(self._avg_vol.current_value)) + + if vol > 0 and q != 0: + self._gamma = (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) + self._kappa = self._gamma / Decimal.exp((2 * min_spread) - 1) + self._latest_parameter_calculation_vol = vol + self.logger().info(f"Gamma: {self._gamma} | Kappa: {self._kappa} | Sigma: {vol}") cdef bint c_is_algorithm_ready(self): - return self._mid_prices.c_is_full() + return self._avg_vol.is_sampling_buffer_full cdef object c_create_base_proposal(self): cdef: @@ -686,20 +680,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): list buys = [] list sells = [] - delta_quantity = self._order_amount - if not self._fixed_order_amount: - base_asset_amount, _ = self.c_get_adjusted_available_balance(self.active_orders) - delta_quantity = self.c_calculate_target_inventory() - float(base_asset_amount) - - self.logger().info(f"delta_quantity:{delta_quantity}") - price = market.c_quantize_order_price(self.trading_pair, Decimal(str(self._optimal_bid))) - size = market.c_quantize_order_amount(self.trading_pair, Decimal(str(abs(delta_quantity)))) + 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, Decimal(str(abs(delta_quantity)))) + size = market.c_quantize_order_amount(self.trading_pair, self._order_amount) if size>0: sells.append(PriceSize(price, size)) @@ -723,35 +710,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return base_balance, quote_balance - cdef c_apply_order_levels_modifiers(self, proposal): - self.c_apply_price_band(proposal) - - cdef c_apply_price_band(self, proposal): - if self._price_ceiling > 0 and self.get_price() >= self._price_ceiling: - proposal.buys = [] - if self._price_floor > 0 and self.get_price() <= self._price_floor: - proposal.sells = [] - cdef c_apply_order_price_modifiers(self, object proposal): - self.c_apply_order_optimization(proposal) - if self._fixed_order_amount: - self.c_apply_order_amount_constraint(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_order_amount_constraint(self, object proposal): - cdef: - ExchangeBase market = self._market_info.market - - for buy in proposal.buys: - buy.size = self._order_amount - for sell in proposal.sells: - sell.size = self._order_amount - - proposal.buys = [o for o in proposal.buys if o.size > 0] - proposal.sells = [o for o in proposal.sells if o.size > 0] - cdef c_apply_budget_constraint(self, object proposal): cdef: ExchangeBase market = self._market_info.market @@ -798,25 +763,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): proposal.sells = [o for o in proposal.sells if o.size > 0] - cdef c_filter_out_takers(self, object proposal): - cdef: - ExchangeBase market = self._market_info.market - list new_buys = [] - list new_sells = [] - top_ask = market.c_get_price(self.trading_pair, True) - if not top_ask.is_nan(): - proposal.buys = [buy for buy in proposal.buys if buy.price < top_ask] - top_bid = market.c_get_price(self.trading_pair, False) - if not top_bid.is_nan(): - proposal.sells = [sell for sell in proposal.sells if sell.price > top_bid] - # 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 - double best_order_spread + object best_order_spread for order in self.active_orders: if order.is_buy: @@ -824,9 +777,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): else: own_sell_size = order.quantity - # 10% of Bid/Ask spread - best_order_spread = self._optimal_spread / 2 * 0.1 - 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( @@ -843,7 +793,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 * Decimal(str(1 - best_order_spread * i))) + 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 @@ -861,7 +811,26 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 * Decimal(str(1 + best_order_spread * i))) + proposal.sells[i].price = market.c_quantize_order_price(self.trading_pair, higher_sell_price) + + cdef c_apply_order_amount_modifiers(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + str trading_pair = self._market_info.trading_pair + + q = market.c_get_available_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: diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index 0342dba693..f5abe02275 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -89,15 +89,6 @@ def validate_price_source_market(value: str) -> Optional[str]: return validate_market_trading_pair(market, value) -def validate_price_floor_ceiling(value: str) -> Optional[str]: - try: - decimal_value = Decimal(value) - except Exception: - return f"{value} is not in decimal format." - if not (decimal_value == Decimal("-1") or decimal_value > Decimal("0")): - return "Value must be more than 0 or -1 to disable this feature." - - def exchange_on_validated(value: str): required_exchanges.append(value) @@ -118,22 +109,68 @@ def exchange_on_validated(value: str): 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=False, + 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, + prompt_on_new=True, + default=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: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True), + "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: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True), "kappa": ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", - type_str="float", - validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), + type_str="decimal", + required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=True), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", - type_str="float", - validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), + type_str="decimal", + required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=True), prompt_on_new=True), + "eta": + ConfigVar(key="eta", + prompt="Enter order amount shape factor (eta) >>> ", + type_str="decimal", + required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1, inclusive=True), + default=Decimal("0.005")), "closing_time": ConfigVar(key="closing_time", - prompt="Enter closing time in days >>> ", - type_str="float", + prompt="Enter algorithm closing time in days. " + "When this time is reached, spread equations will recycle t=0" + " (fractional quantities are allowed i.e. 1.27 days) >>> ", + type_str="decimal", validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), prompt_on_new=True), "order_refresh_time": @@ -154,20 +191,6 @@ def exchange_on_validated(value: str): type_str="float", default=Decimal("1800"), validator=lambda v: validate_decimal(v, 0, inclusive=False)), - "fixed_order_amount": - ConfigVar(key="fixed_order_amount", - prompt="Do you want to create orders with fixed amount? (Alternative is to leave algorithm decide) >>>", - type_str="bool", - default=False, - validator=validate_bool, - prompt_on_new=True), - "order_amount": - ConfigVar(key="order_amount", - prompt=order_amount_prompt, - required_if=lambda: pure_market_making_as_config_map.get("fixed_order_amount").value == "True" and pure_market_making_as_config_map.get("order_amount").value is None, - type_str="decimal", - validator=validate_order_amount, - prompt_on_new=True), "order_refresh_tolerance_pct": ConfigVar(key="order_refresh_tolerance_pct", prompt="Enter the percent change in price needed to refresh orders at each cycle " @@ -239,5 +262,11 @@ def exchange_on_validated(value: str): prompt="Enter amount of samples to use for volatility calculation>>> ", type_str="int", validator=lambda v: validate_decimal(v, 5, 600), - default=Decimal("30")), + default=30), + "buffer_sampling_period": + ConfigVar(key="buffer_sampling_period", + prompt="Enter period in seconds of sampling for volatility calculation>>> ", + type_str="int", + validator=lambda v: validate_decimal(v, 1, 300), + default=30), } diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index cc027e82ec..5cd8c552de 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -19,6 +19,7 @@ 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 @@ -50,15 +51,22 @@ def start(self): asset_price_delegate = APIAssetPriceDelegate(price_source_custom_api) strategy_logging_options = PureMarketMakingASStrategy.OPTION_LOG_ALL - kappa = c_map.get("kappa").value - gamma = c_map.get("gamma").value - closing_time = c_map.get("closing_time").value * 3600 * 24 * 1e3 - fixed_order_amount = c_map.get("fixed_order_amount").value + parameters_based_on_spread = c_map.get("parameters_based_on_spread").value + min_spread = c_map.get("min_spread").value / Decimal(100) + max_spread = c_map.get("max_spread").value / Decimal(100) + if parameters_based_on_spread: + gamma = kappa = -1 + else: + kappa = c_map.get("kappa").value + gamma = c_map.get("gamma").value + closing_time = c_map.get("closing_time").value * Decimal(3600 * 24 * 1e3) buffer_size = c_map.get("buffer_size").value + buffer_sampling_period = c_map.get("buffer_sampling_period").value self.strategy = PureMarketMakingASStrategy( 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, @@ -68,12 +76,15 @@ def start(self): asset_price_delegate=asset_price_delegate, price_type=price_type, hb_app_notification=True, + parameters_based_on_spread=parameters_based_on_spread, + min_spread=min_spread, + max_spread=max_spread, kappa=kappa, gamma=gamma, closing_time=closing_time, - fixed_order_amount=fixed_order_amount, data_path=data_path(), - buffer_size = buffer_size, + buffer_size=buffer_size, + buffer_sampling_period=buffer_sampling_period, ) except Exception as e: self._notify(str(e)) diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 30280193ce..3febd6d9fa 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -15,6 +15,9 @@ market: null # 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 @@ -25,9 +28,6 @@ order_refresh_tolerance_pct: null # Size of your bid and ask order. order_amount: null -# Size of your bid and ask order. -fixed_order_amount: null - # How long to wait before placing the next order in case your order gets filled. filled_order_delay: null @@ -53,12 +53,17 @@ price_source_market: null price_source_custom_api: null # Avellaneda - Stoikov algorithm parameters +parameters_based_on_spread: null +min_spread: null +max_spread: null kappa: null gamma: null +eta: null closing_time: null # Buffer size used to store historic samples and calculate volatility buffer_size: 30 +buffer_sampling_period: 5 # For more detailed information, see: # https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters From 27c5d22bdfde99320f36dafda8a478f2a2a990eb Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 4 Mar 2021 22:19:36 -0300 Subject: [PATCH 20/77] Fixed optimal spread equation to have the constant term multiplied by 2/gamma --- .../pure_market_making_as.pyx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 41947b581f..7211ba801a 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -550,23 +550,33 @@ cdef class PureMarketMakingASStrategy(StrategyBase): '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')]) + 'kappa', + 'current_vol_to_calculation_vol')]) df_header.to_csv(self._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._optimal_bid)/mid_price, + (self._optimal_ask - mid_price) / mid_price, market.c_get_available_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._kappa, + self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value))]) df.to_csv(self._csv_path, mode='a', header=False, index=False) proposal = None @@ -627,7 +637,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): mid_price_variance = Decimal(str(self._avg_vol.current_value)) ** 2 self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) - self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + Decimal(1 + self._gamma / self._kappa).ln() + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma self._optimal_ask = min(self._reserved_price + self._optimal_spread / 2, mid_price * (Decimal(1) + self._max_spread)) self._optimal_bid = max(self._reserved_price - self._optimal_spread / 2, mid_price * (Decimal(1) - self._max_spread)) @@ -667,7 +677,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if vol > 0 and q != 0: self._gamma = (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) - self._kappa = self._gamma / Decimal.exp((2 * min_spread) - 1) + self._kappa = self._gamma / Decimal.exp(min_spread * self._gamma - 1) self._latest_parameter_calculation_vol = vol self.logger().info(f"Gamma: {self._gamma} | Kappa: {self._kappa} | Sigma: {vol}") From 5e83e394612e20b3caafd7409108a2a0bc0d4eda Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 10 Mar 2021 00:07:56 -0300 Subject: [PATCH 21/77] Removing unnecessary code from copy-paste from pmm. Added parameters vol_to_spread_multiplier and inventory_risk_aversion --- .../pure_market_making_as/data_types.py | 5 - .../pure_market_making_as.pxd | 7 +- .../pure_market_making_as.pyx | 324 +++++------------- .../pure_market_making_as_config_map.py | 33 +- .../strategy/pure_market_making_as/start.py | 12 +- ...ure_market_making_as_strategy_TEMPLATE.yml | 8 +- 6 files changed, 136 insertions(+), 253 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/data_types.py b/hummingbot/strategy/pure_market_making_as/data_types.py index 4a8c1f5d04..ce86cd6091 100644 --- a/hummingbot/strategy/pure_market_making_as/data_types.py +++ b/hummingbot/strategy/pure_market_making_as/data_types.py @@ -31,11 +31,6 @@ class SizingProposal(NamedTuple): sell_order_sizes: List[Decimal] -class InventorySkewBidAskRatios(NamedTuple): - bid_ratio: float - ask_ratio: float - - class PriceSize: def __init__(self, price: Decimal, size: Decimal): self.price: Decimal = price diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index 597dea7f5b..a95341feb7 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -15,8 +15,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object _order_refresh_tolerance_pct double _filled_order_delay object _inventory_target_base_pct - bint _hanging_orders_enabled - object _hanging_orders_cancel_pct bint _order_optimization_enabled bint _add_transaction_costs_to_orders object _asset_price_delegate @@ -30,17 +28,17 @@ cdef class PureMarketMakingASStrategy(StrategyBase): bint _all_markets_ready int _filled_buys_balance int _filled_sells_balance - list _hanging_order_ids double _last_timestamp double _status_report_interval int64_t _logging_options object _last_own_trade_price - list _hanging_aged_order_prices int _buffer_sampling_period double _last_sampling_timestamp bint _parameters_based_on_spread object _min_spread object _max_spread + object _vol_to_spread_multiplier + object _inventory_risk_aversion object _kappa object _gamma object _eta @@ -65,7 +63,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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_cancel_hanging_orders(self) cdef c_aged_order_refresh(self) cdef bint c_to_create_orders(self, object proposal) cdef c_execute_orders_proposal(self, object proposal) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 7211ba801a..5702d85254 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -1,18 +1,17 @@ from decimal import Decimal import logging -import os.path import pandas as pd import numpy as np from typing import ( List, Dict, - Optional ) from math import ( floor, ceil ) import time +import os from hummingbot.core.clock cimport Clock from hummingbot.core.event.events import TradeType, PriceType from hummingbot.core.data_type.limit_order cimport LimitOrder @@ -78,11 +77,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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"), kappa: Decimal = Decimal("0.1"), gamma: Decimal = Decimal("0.5"), eta: Decimal = Decimal("0.005"), closing_time: Decimal = Decimal("86400000"), - data_path: str = '', + csv_path: str = '', buffer_size: int = 30, buffer_sampling_period: int = 60 ): @@ -103,12 +104,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._cancel_timestamp = 0 self._create_timestamp = 0 - self._hanging_aged_order_prices = [] 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._hanging_order_ids = [] self._logging_options = logging_options self._last_timestamp = 0 self._status_report_interval = status_report_interval @@ -118,6 +117,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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(buffer_size, buffer_size) self._buffer_sampling_period = buffer_sampling_period self._last_sampling_timestamp = 0 @@ -131,8 +132,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_spread = s_decimal_zero self._optimal_ask = s_decimal_zero self._optimal_bid = s_decimal_zero - - self._csv_path = os.path.join(data_path, f"PMM_AS_{pd.Timestamp.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv") + self._csv_path = csv_path try: os.unlink(self._csv_path) except FileNotFoundError: @@ -161,40 +161,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def order_amount(self, value: Decimal): self._order_amount = value - @property - def order_levels(self) -> int: - return self._order_levels - - @order_levels.setter - def order_levels(self, value: int): - self._order_levels = value - self._buy_levels = value - self._sell_levels = value - - @property - def buy_levels(self) -> int: - return self._buy_levels - - @buy_levels.setter - def buy_levels(self, value: int): - self._buy_levels = value - - @property - def sell_levels(self) -> int: - return self._sell_levels - - @sell_levels.setter - def sell_levels(self, value: int): - self._sell_levels = value - - @property - def order_level_amount(self) -> Decimal: - return self._order_level_amount - - @order_level_amount.setter - def order_level_amount(self, value: Decimal): - self._order_level_amount = value - @property def inventory_target_base_pct(self) -> Decimal: return self._inventory_target_base_pct @@ -203,22 +169,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def inventory_target_base_pct(self, value: Decimal): self._inventory_target_base_pct = value - @property - def hanging_orders_enabled(self) -> bool: - return self._hanging_orders_enabled - - @hanging_orders_enabled.setter - def hanging_orders_enabled(self, value: bool): - self._hanging_orders_enabled = value - - @property - def hanging_orders_cancel_pct(self) -> Decimal: - return self._hanging_orders_cancel_pct - - @hanging_orders_cancel_pct.setter - def hanging_orders_cancel_pct(self, value: Decimal): - self._hanging_orders_cancel_pct = value - @property def order_optimization_enabled(self) -> bool: return self._order_optimization_enabled @@ -301,10 +251,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): mid_price = self._market_info.get_mid_price() return mid_price - @property - def hanging_order_ids(self) -> List[str]: - return self._hanging_order_ids - @property def market_info_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: return self._sb_order_tracker.market_pair_to_active_orders @@ -323,11 +269,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def active_sells(self) -> List[LimitOrder]: return [o for o in self.active_orders if not o.is_buy] - @property - def active_non_hanging_orders(self) -> List[LimitOrder]: - orders = [o for o in self.active_orders if o.client_order_id not in self._hanging_order_ids] - return orders - @property def logging_options(self) -> int: return self._logging_options @@ -344,14 +285,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def asset_price_delegate(self, value): self._asset_price_delegate = value - @property - def inventory_cost_price_delegate(self) -> AssetPriceDelegate: - return self._inventory_cost_price_delegate - - @inventory_cost_price_delegate.setter - def inventory_cost_price_delegate(self, value): - self._inventory_cost_price_delegate = value - @property def order_tracker(self): return self._sb_order_tracker @@ -382,28 +315,19 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 not in self._hanging_order_ids]) + 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] - level = None - if order.client_order_id not in self._hanging_order_ids: - if order.is_buy: - level = lvl_buy + 1 - lvl_buy += 1 - else: - level = no_sells - lvl_sell - lvl_sell += 1 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([ "", @@ -430,12 +354,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): bid_price = market.get_price(trading_pair, False) ask_price = market.get_price(trading_pair, True) ref_price = float("nan") - if market == self._market_info.market and self._inventory_cost_price_delegate is not None: - # We're using inventory_cost, show it's price - ref_price = self._inventory_cost_price_delegate.get_price() - if ref_price is None: - ref_price = self.get_price() - elif market == self._market_info.market and self._asset_price_delegate is None: + if market == self._market_info.market and self._asset_price_delegate is None: ref_price = self.get_price() elif ( self._asset_price_delegate is not None @@ -470,7 +389,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): formatters={0: ("{:<" + str(first_col_length) + "}").format}).split("\n") lines.extend(["", " Assets:"] + [" " + line for line in df_lines]) - # See if there're any open orders. + # 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")]) @@ -502,9 +421,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._last_timestamp = timestamp # start tracking any restored limit order restored_order_ids = self.c_track_restored_orders(self.market_info) - # make restored order hanging orders - for order_id in restored_order_ids: - self._hanging_order_ids.append(order_id) self._time_left = self._closing_time cdef c_tick(self, double timestamp): @@ -515,7 +431,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): bint should_report_warnings = ((current_tick > last_tick) and (self._logging_options & self.OPTION_LOG_STATUS_REPORT)) cdef object proposal - ExchangeBase market = self._market_info.market try: if not self._all_markets_ready: self._all_markets_ready = all([mkt.ready for mkt in self._sb_markets]) @@ -534,50 +449,15 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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 == s_decimal_neg_one or self._kappa == s_decimal_neg_one) or \ - (self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > 0.3): + (self._parameters_based_on_spread and + self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > (self._vol_to_spread_multiplier - 1)): self.c_recalculate_parameters() self.c_calculate_reserved_price_and_optimal_spread() - mid_price = self.c_get_mid_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._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', - 'current_vol_to_calculation_vol')]) - df_header.to_csv(self._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._optimal_bid)/mid_price, - (self._optimal_ask - mid_price) / mid_price, - market.c_get_available_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.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value))]) - df.to_csv(self._csv_path, mode='a', header=False, index=False) + self.dump_debug_variables() proposal = None if self._create_timestamp <= self._current_timestamp: @@ -591,7 +471,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_apply_budget_constraint(proposal) self.c_cancel_active_orders(proposal) - self.c_cancel_hanging_orders() refresh_proposal = self.c_aged_order_refresh() # Firstly restore cancelled aged order if refresh_proposal is not None: @@ -623,7 +502,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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)) + return market.c_get_price(trading_pair, True) - market.c_get_price(trading_pair, False) cdef c_calculate_reserved_price_and_optimal_spread(self): cdef: @@ -634,13 +513,17 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if self.c_is_algorithm_ready(): mid_price = self.c_get_mid_price() q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) - mid_price_variance = Decimal(str(self._avg_vol.current_value)) ** 2 + vol = Decimal(str(self._avg_vol.current_value)) + mid_price_variance = vol ** 2 self._reserved_price = mid_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 - self._optimal_ask = min(self._reserved_price + self._optimal_spread / 2, mid_price * (Decimal(1) + self._max_spread)) - self._optimal_bid = max(self._reserved_price - self._optimal_spread / 2, mid_price * (Decimal(1) - self._max_spread)) - + self._optimal_ask = min(max(self._reserved_price + self._optimal_spread / 2, + mid_price * (1 + self._min_spread)), + mid_price * (1 + self._max_spread)) + self._optimal_bid = min(max(self._reserved_price - self._optimal_spread / 2, + mid_price * (1 - self._max_spread)), + mid_price * (1 - self._min_spread)) self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " f"q={q:.4f} | " @@ -658,7 +541,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object target_inventory_value mid_price = self.c_get_mid_price() - # Need to review this to see if adjusted quantities are required base_asset_amount = market.get_balance(base_asset) quote_asset_amount = market.get_balance(quote_asset) base_value = base_asset_amount * mid_price @@ -671,15 +553,25 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ExchangeBase market = self._market_info.market q = market.c_get_available_balance(self.base_asset) - self.c_calculate_target_inventory() - min_spread = self._min_spread * self.c_get_mid_price() - max_spread = self._max_spread * self.c_get_mid_price() vol = Decimal(str(self._avg_vol.current_value)) + mid_price=self.c_get_mid_price() if vol > 0 and q != 0: - self._gamma = (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) - self._kappa = self._gamma / Decimal.exp(min_spread * self._gamma - 1) + # Initially min_spread and max_spread defined by user will be used, but both of them will be modified by vol_to_spread_multiplier if vol too big + min_spread = max(self._min_spread * mid_price, self._vol_to_spread_multiplier * vol) + max_spread = max(self._max_spread * mid_price, self._vol_to_spread_multiplier * vol + (self._max_spread - self._min_spread) * mid_price) + + self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) / 2 + + # Want the minimum possible spread which ideally is 2*min_spread, + # but with restrictions to avoid negative kappa or division by 0 + if (2 * min_spread) <= 2 * self._gamma * (vol ** 2): + self._kappa = Decimal('Inf') + else: + self._kappa = self._gamma / (Decimal.exp((2 * min_spread * self._gamma) / 2) - 1) + self._latest_parameter_calculation_vol = vol - self.logger().info(f"Gamma: {self._gamma} | Kappa: {self._kappa} | Sigma: {vol}") + self.logger().info(f"Gamma: {self._gamma:.5f} | Kappa: {self._kappa:.5f} | Sigma: {vol:.5f}") cdef bint c_is_algorithm_ready(self): return self._avg_vol.is_sampling_buffer_full @@ -734,7 +626,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object base_size object adjusted_amount - base_balance, quote_balance = self.c_get_adjusted_available_balance(self.active_non_hanging_orders) + 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, @@ -745,8 +637,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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) - # self.logger().info(f"Not enough balance for buy order (Size: {buy.size.normalize()}, Price: {buy.price.normalize()}), " - # f"order_amount is adjusted to {adjusted_amount}") buy.size = adjusted_amount quote_balance = s_decimal_zero elif quote_balance == s_decimal_zero: @@ -762,8 +652,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): # 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) - # self.logger().info(f"Not enough balance for sell order (Size: {sell.size.normalize()}, Price: {sell.price.normalize()}), " - # f"order_amount is adjusted to {adjusted_amount}") sell.size = adjusted_amount base_balance = s_decimal_zero elif base_balance == s_decimal_zero: @@ -828,7 +716,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): ExchangeBase market = self._market_info.market str trading_pair = self._market_info.trading_pair - q = market.c_get_available_balance(self.base_asset) - self.c_calculate_target_inventory() + # 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): @@ -881,9 +771,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): f"{order_filled_event.amount} {market_info.base_asset} filled." ) - if self._inventory_cost_price_delegate is not None: - self._inventory_cost_price_delegate.process_order_fill_event(order_filled_event) - cdef c_did_complete_buy_order(self, object order_completed_event): cdef: str order_id = order_completed_event.order_id @@ -892,29 +779,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return active_sell_ids = [x.client_order_id for x in self.active_orders if not x.is_buy] - if self._hanging_orders_enabled: - # If the filled order is a hanging order, do nothing - if order_id in self._hanging_order_ids: - self.log_with_clock( - logging.INFO, - f"({self.trading_pair}) Hanging 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"Hanging maker BUY order {limit_order_record.quantity} {limit_order_record.base_currency} @ " - f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." - ) - return - - # delay order creation by filled_order_dalay (in seconds) + # 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) - if self._hanging_orders_enabled: - for other_order_id in active_sell_ids: - self._hanging_order_ids.append(other_order_id) - self._filled_buys_balance += 1 self._last_own_trade_price = limit_order_record.price @@ -936,29 +804,11 @@ cdef class PureMarketMakingASStrategy(StrategyBase): if limit_order_record is None: return active_buy_ids = [x.client_order_id for x in self.active_orders if x.is_buy] - if self._hanging_orders_enabled: - # If the filled order is a hanging order, do nothing - if order_id in self._hanging_order_ids: - self.log_with_clock( - logging.INFO, - f"({self.trading_pair}) Hanging 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"Hanging maker SELL order {limit_order_record.quantity} {limit_order_record.base_currency} @ " - f"{limit_order_record.price} {limit_order_record.quote_currency} is filled." - ) - return - # delay order creation by filled_order_dalay (in seconds) + # 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) - if self._hanging_orders_enabled: - for other_order_id in active_buy_ids: - self._hanging_order_ids.append(other_order_id) - self._filled_sells_balance += 1 self._last_own_trade_price = limit_order_record.price @@ -984,7 +834,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return False return True - # Cancel active non hanging orders + # 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: @@ -995,14 +845,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return cdef: - list active_orders = self.active_non_hanging_orders + 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] @@ -1015,29 +864,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): for order in active_orders: self.c_cancel_order(self._market_info, order.client_order_id) else: - # self.logger().info(f"Not cancelling active orders since difference between new order prices " - # f"and current order prices is within " - # f"{self._order_refresh_tolerance_pct:.2%} order_refresh_tolerance_pct") self.set_timers() - cdef c_cancel_hanging_orders(self): - 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: - object price = self.get_price() - list active_orders = self.active_orders - list orders - LimitOrder order - for h_order_id in self._hanging_order_ids: - orders = [o for o in active_orders if o.client_order_id == h_order_id] - if orders and price > 0: - order = orders[0] - if abs(order.price - price)/price >= self._hanging_orders_cancel_pct: - self.c_cancel_order(self._market_info, order.client_order_id) - # Refresh all active order that are older that the _max_order_age cdef c_aged_order_refresh(self): cdef: @@ -1060,8 +888,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): buys.append(PriceSize(order.price, order.quantity)) else: sells.append(PriceSize(order.price, order.quantity)) - if order.client_order_id in self._hanging_order_ids: - self._hanging_aged_order_prices.append(order.price) 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.") @@ -1070,8 +896,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef bint c_to_create_orders(self, object proposal): return self._create_timestamp < self._current_timestamp and \ - proposal is not None and \ - len(self.active_non_hanging_orders) == 0 + proposal is not None cdef c_execute_orders_proposal(self, object proposal): cdef: @@ -1100,9 +925,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): price=buy.price, expiration_seconds=expiration_seconds ) - if buy.price in self._hanging_aged_order_prices: - self._hanging_order_ids.append(bid_order_id) - self._hanging_aged_order_prices.remove(buy.price) orders_created = True if len(proposal.sells) > 0: if self._logging_options & self.OPTION_LOG_CREATE_ORDER: @@ -1121,9 +943,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): price=sell.price, expiration_seconds=expiration_seconds ) - if sell.price in self._hanging_aged_order_prices: - self._hanging_order_ids.append(ask_order_id) - self._hanging_aged_order_prices.remove(sell.price) orders_created = True if orders_created: self.set_timers() @@ -1155,3 +974,48 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return PriceType.InventoryCost else: raise ValueError(f"Unrecognized price type string {price_type_str}.") + + def dump_debug_variables(self): + market = self._market_info.market + mid_price = self.c_get_mid_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._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', + 'current_vol_to_calculation_vol', + 'inventory_target_pct')]) + df_header.to_csv(self._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.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value), + self.inventory_target_base_pct)]) + df.to_csv(self._csv_path, mode='a', header=False, index=False) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index f5abe02275..907ae13210 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -119,14 +119,13 @@ def exchange_on_validated(value: str): ConfigVar(key="order_optimization_enabled", prompt="Do you want to enable best bid ask jumping? (Yes/No) >>> ", type_str="bool", - default=False, + 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, - prompt_on_new=True, default=True), "min_spread": ConfigVar(key="min_spread", @@ -144,19 +143,35 @@ def exchange_on_validated(value: str): required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), prompt_on_new=True), + "vol_to_spread_multiplier": + ConfigVar(key="vol_to_spread_multiplier", + prompt="Enter the Volatility-to-Spread multiplier: " + "Beyond this number of sigmas, spreads will turn into multiples of volatility >>>", + type_str="decimal", + required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), + prompt_on_new=True), + "inventory_risk_aversion": + ConfigVar(key="inventory_risk_aversion", + prompt="Enter Inventory risk aversion: With 1.0 being extremely conservative about meeting inventory target, " + "at the expense of profit, and 0.0 for a profit driven, at the expense of inventory risk >>>", + type_str="decimal", + required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), + prompt_on_new=True), "kappa": ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", type_str="decimal", required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, - validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=True), + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", type_str="decimal", required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, - validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=True), + validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "eta": ConfigVar(key="eta", @@ -172,7 +187,7 @@ def exchange_on_validated(value: str): " (fractional quantities are allowed i.e. 1.27 days) >>> ", type_str="decimal", validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), - prompt_on_new=True), + default=Decimal("1")), "order_refresh_time": ConfigVar(key="order_refresh_time", prompt="How often do you want to cancel and replace bids and asks " @@ -210,6 +225,7 @@ def exchange_on_validated(value: str): prompt="What is your target base asset percentage? 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", @@ -227,7 +243,7 @@ def exchange_on_validated(value: str): "price_type": ConfigVar(key="price_type", prompt="Which price type to use? (" - "mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost) >>> ", + "mid_price/last_price/last_own_trade_price/best_bid/best_ask) >>> ", type_str="str", required_if=lambda: pure_market_making_as_config_map.get("price_source").value != "custom_api", default="mid_price", @@ -236,7 +252,6 @@ def exchange_on_validated(value: str): "last_own_trade_price", "best_bid", "best_ask", - "inventory_cost", } else "Invalid price type."), "price_source_exchange": @@ -262,11 +277,11 @@ def exchange_on_validated(value: str): prompt="Enter amount of samples to use for volatility calculation>>> ", type_str="int", validator=lambda v: validate_decimal(v, 5, 600), - default=30), + default=60), "buffer_sampling_period": ConfigVar(key="buffer_sampling_period", prompt="Enter period in seconds of sampling for volatility calculation>>> ", type_str="int", validator=lambda v: validate_decimal(v, 1, 300), - default=30), + default=1), } diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index 5cd8c552de..f63499e958 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -4,6 +4,8 @@ ) 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.pure_market_making_as import ( PureMarketMakingASStrategy, @@ -14,6 +16,7 @@ from hummingbot.connector.exchange.paper_trade import create_paper_trade_market from hummingbot.connector.exchange_base import ExchangeBase from decimal import Decimal +import pandas as pd def start(self): @@ -54,6 +57,8 @@ def start(self): parameters_based_on_spread = c_map.get("parameters_based_on_spread").value 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 if parameters_based_on_spread: gamma = kappa = -1 else: @@ -62,6 +67,9 @@ def start(self): closing_time = c_map.get("closing_time").value * Decimal(3600 * 24 * 1e3) buffer_size = c_map.get("buffer_size").value buffer_sampling_period = c_map.get("buffer_sampling_period").value + 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 = PureMarketMakingASStrategy( market_info=MarketTradingPairTuple(*maker_data), @@ -79,10 +87,12 @@ def start(self): 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, kappa=kappa, gamma=gamma, closing_time=closing_time, - data_path=data_path(), + csv_path=csv_path, buffer_size=buffer_size, buffer_sampling_period=buffer_sampling_period, ) diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index 3febd6d9fa..f85ec0953f 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -56,14 +56,16 @@ price_source_custom_api: null parameters_based_on_spread: null min_spread: null max_spread: null +vol_to_spread_multiplier: null +inventory_risk_aversion: null kappa: null gamma: null -eta: null +eta: 0.005 closing_time: null # Buffer size used to store historic samples and calculate volatility -buffer_size: 30 -buffer_sampling_period: 5 +buffer_size: 60 +buffer_sampling_period: 1 # For more detailed information, see: # https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters From 90e142ee50e77d9212f4d60b20e2adf0898093bf Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 10 Mar 2021 20:30:15 -0300 Subject: [PATCH 22/77] refactored how optimal bid/ask are calculated --- .../pure_market_making_as.pyx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 5702d85254..e3ba0eef58 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -517,13 +517,18 @@ cdef class PureMarketMakingASStrategy(StrategyBase): mid_price_variance = vol ** 2 self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + min_limit_bid = min(mid_price * (1 - self._max_spread), mid_price - self._vol_to_spread_multiplier * vol) + max_limit_bid = mid_price * (1 - self._min_spread) + min_limit_ask = mid_price * (1 + self._min_spread) + max_limit_ask = max(mid_price * (1 + self._max_spread), mid_price + self._vol_to_spread_multiplier * vol) + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma self._optimal_ask = min(max(self._reserved_price + self._optimal_spread / 2, - mid_price * (1 + self._min_spread)), - mid_price * (1 + self._max_spread)) + min_limit_ask), + max_limit_ask) self._optimal_bid = min(max(self._reserved_price - self._optimal_spread / 2, - mid_price * (1 - self._max_spread)), - mid_price * (1 - self._min_spread)) + min_limit_bid), + max_limit_bid) self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " f"q={q:.4f} | " @@ -970,8 +975,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return PriceType.LastTrade elif price_type_str == 'last_own_trade_price': return PriceType.LastOwnTrade - elif price_type_str == 'inventory_cost': - return PriceType.InventoryCost else: raise ValueError(f"Unrecognized price type string {price_type_str}.") From 3f6ef33f3799f04992e708c88ef20df6e1a50bbc Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 10 Mar 2021 20:31:35 -0300 Subject: [PATCH 23/77] removed unnecessary check --- .../pure_market_making_as.pyx | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index e3ba0eef58..31baeccae2 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -510,29 +510,28 @@ cdef class PureMarketMakingASStrategy(StrategyBase): time_left_fraction = Decimal(str(self._time_left / self._closing_time)) - if self.c_is_algorithm_ready(): - mid_price = self.c_get_mid_price() - q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) - vol = Decimal(str(self._avg_vol.current_value)) - mid_price_variance = vol ** 2 - self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) - - min_limit_bid = min(mid_price * (1 - self._max_spread), mid_price - self._vol_to_spread_multiplier * vol) - max_limit_bid = mid_price * (1 - self._min_spread) - min_limit_ask = mid_price * (1 + self._min_spread) - max_limit_ask = max(mid_price * (1 + self._max_spread), mid_price + self._vol_to_spread_multiplier * vol) - - self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma - 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) - self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " - f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " - f"q={q:.4f} | " - f"sigma2={mid_price_variance:.4f}") + mid_price = self.c_get_mid_price() + q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) + vol = Decimal(str(self._avg_vol.current_value)) + mid_price_variance = vol ** 2 + self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + + min_limit_bid = min(mid_price * (1 - self._max_spread), mid_price - self._vol_to_spread_multiplier * vol) + max_limit_bid = mid_price * (1 - self._min_spread) + min_limit_ask = mid_price * (1 + self._min_spread) + max_limit_ask = max(mid_price * (1 + self._max_spread), mid_price + self._vol_to_spread_multiplier * vol) + + self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma + 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) + self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " + f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " + f"q={q:.4f} | " + f"sigma2={mid_price_variance:.4f}") cdef object c_calculate_target_inventory(self): cdef: From 72f7444dc47a77c1f42613575ea63822c6056b6e Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 10 Mar 2021 22:15:48 -0300 Subject: [PATCH 24/77] Added debug variables --- .../pure_market_making_as/pure_market_making_as.pyx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 31baeccae2..348216bcda 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -1002,7 +1002,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 'gamma', 'kappa', 'current_vol_to_calculation_vol', - 'inventory_target_pct')]) + 'inventory_target_pct', + 'min_spread', + 'max_spread', + 'vol_to_spread_multiplier')]) df_header.to_csv(self._csv_path, mode='a', header=False, index=False) df = pd.DataFrame([(mid_price, spread, @@ -1019,5 +1022,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._gamma, self._kappa, self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value), - self.inventory_target_base_pct)]) + self.inventory_target_base_pct, + self._min_spread, + self._max_spread, + self._vol_to_spread_multiplier)]) df.to_csv(self._csv_path, mode='a', header=False, index=False) From 39c1acafabdbcfd30f98f3ad64bd96cdb7817aa5 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 10 Mar 2021 23:58:35 -0300 Subject: [PATCH 25/77] Fixed bug in the kappa calculation --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 348216bcda..07e9e41132 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -569,10 +569,11 @@ cdef class PureMarketMakingASStrategy(StrategyBase): # Want the minimum possible spread which ideally is 2*min_spread, # but with restrictions to avoid negative kappa or division by 0 - if (2 * min_spread) <= 2 * self._gamma * (vol ** 2): + max_spread_around_reserved_price = 2 * (max_spread - q * self._gamma * (vol ** 2)) + if max_spread_around_reserved_price <= self._gamma * (vol ** 2): self._kappa = Decimal('Inf') else: - self._kappa = self._gamma / (Decimal.exp((2 * min_spread * self._gamma) / 2) - 1) + self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) self._latest_parameter_calculation_vol = vol self.logger().info(f"Gamma: {self._gamma:.5f} | Kappa: {self._kappa:.5f} | Sigma: {vol:.5f}") From 50fc8a1289d683f2edc343a0f787ef4e0e34061f Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Sat, 13 Mar 2021 19:28:47 -0300 Subject: [PATCH 26/77] Changed get_mid_price to get_price in case price_type is changed. Also fixed bug where add_transaction_costs were defaulted to True --- .../pure_market_making_as.pyx | 53 +++++++++++-------- .../strategy/pure_market_making_as/start.py | 3 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 07e9e41132..c639d41d2a 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -119,7 +119,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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(buffer_size, buffer_size) + self._avg_vol=AverageVolatilityIndicator(buffer_size, 3) self._buffer_sampling_period = buffer_sampling_period self._last_sampling_timestamp = 0 self._kappa = kappa @@ -396,8 +396,8 @@ cdef class PureMarketMakingASStrategy(StrategyBase): else: lines.extend(["", " No active maker orders."]) - volatility_pct = self._avg_vol.current_value / float(self.c_get_mid_price()) * 100.0 - lines.extend(["", f"Avellaneda-Stoikov: Gamma= {self._gamma:.5E} | Kappa= {self._kappa:.5E} | Volatility= {volatility_pct:.3f}%"]) + volatility_pct = self._avg_vol.current_value / float(self.get_price()) * 100.0 + lines.extend(["", f"Avellaneda-Stoikov: Gamma= {self._gamma:.5E} | Kappa= {self._kappa:.5E} | Volatility= {volatility_pct:.3f}% | Time left fraction= {self._time_left/self._closing_time:.4f}"]) warning_lines.extend(self.balance_warning([self._market_info])) @@ -477,12 +477,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_execute_orders_proposal(refresh_proposal) if self.c_to_create_orders(proposal): self.c_execute_orders_proposal(proposal) + else: + self.logger().info(f"Algorithm not ready...") finally: self._last_timestamp = timestamp cdef c_collect_market_variables(self, double timestamp): if timestamp - self._last_sampling_timestamp >= self._buffer_sampling_period: - self._avg_vol.add_sample(self.c_get_mid_price()) + self._avg_vol.add_sample(self.get_price()) self._last_sampling_timestamp = timestamp self._time_left = max(self._time_left - Decimal(timestamp - self._last_timestamp) * 1000, 0) if self._time_left == 0: @@ -510,16 +512,16 @@ cdef class PureMarketMakingASStrategy(StrategyBase): time_left_fraction = Decimal(str(self._time_left / self._closing_time)) - mid_price = self.c_get_mid_price() + price = self.get_price() q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) vol = Decimal(str(self._avg_vol.current_value)) mid_price_variance = vol ** 2 - self._reserved_price = mid_price - (q * self._gamma * mid_price_variance * time_left_fraction) + self._reserved_price = price - (q * self._gamma * mid_price_variance * time_left_fraction) - min_limit_bid = min(mid_price * (1 - self._max_spread), mid_price - self._vol_to_spread_multiplier * vol) - max_limit_bid = mid_price * (1 - self._min_spread) - min_limit_ask = mid_price * (1 + self._min_spread) - max_limit_ask = max(mid_price * (1 + self._max_spread), mid_price + self._vol_to_spread_multiplier * vol) + 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) self._optimal_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma self._optimal_ask = min(max(self._reserved_price + self._optimal_spread / 2, @@ -528,10 +530,15 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_bid = min(max(self._reserved_price - self._optimal_spread / 2, min_limit_bid), max_limit_bid) - self.logger().info(f"bid={(mid_price-(self._reserved_price - self._optimal_spread / 2))/mid_price*100:.4f}% | " - f"ask={((self._reserved_price + self._optimal_spread / 2)-mid_price)/mid_price*100:.4f}% | " + # 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 + 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"vol_based_bid/ask={self._vol_to_spread_multiplier * vol / price * 100:.4f}% | " + f"opt_bid={(price-self._optimal_bid) / price * 100:.4f}% | " + f"opt_ask={(self._optimal_ask-price) / price * 100:.4f}% | " f"q={q:.4f} | " - f"sigma2={mid_price_variance:.4f}") + f"vol={vol:.4f}") cdef object c_calculate_target_inventory(self): cdef: @@ -544,13 +551,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object inventory_value object target_inventory_value - mid_price = self.c_get_mid_price() + 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 * mid_price + 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 / mid_price))) + return market.c_quantize_order_amount(trading_pair, Decimal(str(target_inventory_value / price))) cdef c_recalculate_parameters(self): cdef: @@ -558,25 +565,25 @@ cdef class PureMarketMakingASStrategy(StrategyBase): q = market.c_get_available_balance(self.base_asset) - self.c_calculate_target_inventory() vol = Decimal(str(self._avg_vol.current_value)) - mid_price=self.c_get_mid_price() + price=self.get_price() if vol > 0 and q != 0: # Initially min_spread and max_spread defined by user will be used, but both of them will be modified by vol_to_spread_multiplier if vol too big - min_spread = max(self._min_spread * mid_price, self._vol_to_spread_multiplier * vol) - max_spread = max(self._max_spread * mid_price, self._vol_to_spread_multiplier * vol + (self._max_spread - self._min_spread) * mid_price) + min_spread = self._min_spread * price + max_spread = self._max_spread * price + # If volatility is too high, gamma -> 0. Is this desirable? self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) / 2 - # Want the minimum possible spread which ideally is 2*min_spread, + # Want the maximum possible spread which ideally is 2 * max_spread minus (the shift between reserved and price)/2, # but with restrictions to avoid negative kappa or division by 0 - max_spread_around_reserved_price = 2 * (max_spread - q * self._gamma * (vol ** 2)) + max_spread_around_reserved_price = 2 * max_spread - q * self._gamma * (vol ** 2) if max_spread_around_reserved_price <= self._gamma * (vol ** 2): self._kappa = Decimal('Inf') else: self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) self._latest_parameter_calculation_vol = vol - self.logger().info(f"Gamma: {self._gamma:.5f} | Kappa: {self._kappa:.5f} | Sigma: {vol:.5f}") cdef bint c_is_algorithm_ready(self): return self._avg_vol.is_sampling_buffer_full @@ -980,7 +987,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def dump_debug_variables(self): market = self._market_info.market - mid_price = self.c_get_mid_price() + mid_price = self.get_price() spread = Decimal(str(self.c_get_spread())) best_ask = mid_price + spread / 2 diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index f63499e958..f0be0dfe09 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -35,6 +35,7 @@ def start(self): price_source_custom_api = c_map.get("price_source_custom_api").value 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] @@ -79,7 +80,7 @@ def start(self): 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=True, + add_transaction_costs_to_orders=add_transaction_costs_to_orders, logging_options=strategy_logging_options, asset_price_delegate=asset_price_delegate, price_type=price_type, From 1fb8751c361d716caa4a35518715d053af963411 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Sun, 14 Mar 2021 23:58:01 -0300 Subject: [PATCH 27/77] Corrected small mistake in the calculation of kappa --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index c639d41d2a..77901b7778 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -577,7 +577,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): # Want the maximum possible spread which ideally is 2 * max_spread minus (the shift between reserved and price)/2, # but with restrictions to avoid negative kappa or division by 0 - max_spread_around_reserved_price = 2 * max_spread - q * self._gamma * (vol ** 2) + max_spread_around_reserved_price = 2 * max_spread - 2 * q * self._gamma * (vol ** 2) if max_spread_around_reserved_price <= self._gamma * (vol ** 2): self._kappa = Decimal('Inf') else: From 5d0bdb705dd6405aaa70a30773eb0a205f6f0a0c Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 15 Mar 2021 12:49:39 -0300 Subject: [PATCH 28/77] Fixed bug in the calculation of max_spread_around_reserved_price --- .../pure_market_making_as/pure_market_making_as.pyx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 77901b7778..ca935bb711 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -573,11 +573,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): max_spread = self._max_spread * price # If volatility is too high, gamma -> 0. Is this desirable? - self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) / 2 + self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) - # Want the maximum possible spread which ideally is 2 * max_spread minus (the shift between reserved and price)/2, - # but with restrictions to avoid negative kappa or division by 0 - max_spread_around_reserved_price = 2 * max_spread - 2 * q * self._gamma * (vol ** 2) + # Want the maximum possible spread but with restrictions to avoid negative kappa or division by 0 + max_spread_around_reserved_price = max_spread + min_spread if max_spread_around_reserved_price <= self._gamma * (vol ** 2): self._kappa = Decimal('Inf') else: From da85c1f9183ca6fa408c891885bf1357e9d73c6c Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 15 Mar 2021 17:48:48 -0300 Subject: [PATCH 29/77] Corrected calculations of inventory using available balance to use total balance instead --- .../strategy/pure_market_making_as/pure_market_making_as.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index ca935bb711..1cbc332a07 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -513,7 +513,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): time_left_fraction = Decimal(str(self._time_left / self._closing_time)) price = self.get_price() - q = market.c_get_available_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) + q = market.get_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) vol = Decimal(str(self._avg_vol.current_value)) mid_price_variance = vol ** 2 self._reserved_price = price - (q * self._gamma * mid_price_variance * time_left_fraction) @@ -563,7 +563,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef: ExchangeBase market = self._market_info.market - q = market.c_get_available_balance(self.base_asset) - self.c_calculate_target_inventory() + q = market.get_balance(self.base_asset) - self.c_calculate_target_inventory() vol = Decimal(str(self._avg_vol.current_value)) price=self.get_price() From cd37550a1429c88b846a83021f22cbcfa4abf916 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Tue, 16 Mar 2021 17:15:13 -0300 Subject: [PATCH 30/77] Removed Price types. Changed to dynamically calculated eta in spread_based approach. Corrected max_spread_around_reserved_price to include IRA --- .../pure_market_making_as/__init__.py | 6 -- .../api_asset_price_delegate.pxd | 4 - .../api_asset_price_delegate.pyx | 19 ---- .../asset_price_delegate.pxd | 3 - .../asset_price_delegate.pyx | 16 ---- .../order_book_asset_price_delegate.pxd | 7 -- .../order_book_asset_price_delegate.pyx | 29 ------ .../pure_market_making_as.pxd | 4 +- .../pure_market_making_as.pyx | 88 +++++-------------- .../pure_market_making_as_config_map.py | 78 ++-------------- .../strategy/pure_market_making_as/start.py | 23 +---- ...ure_market_making_as_strategy_TEMPLATE.yml | 17 +--- 12 files changed, 33 insertions(+), 261 deletions(-) delete mode 100644 hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd delete mode 100644 hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx delete mode 100644 hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd delete mode 100644 hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx delete mode 100644 hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd delete mode 100644 hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx diff --git a/hummingbot/strategy/pure_market_making_as/__init__.py b/hummingbot/strategy/pure_market_making_as/__init__.py index 598802efe2..337540fe0f 100644 --- a/hummingbot/strategy/pure_market_making_as/__init__.py +++ b/hummingbot/strategy/pure_market_making_as/__init__.py @@ -1,12 +1,6 @@ #!/usr/bin/env python from .pure_market_making_as import PureMarketMakingASStrategy -from .asset_price_delegate import AssetPriceDelegate -from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate -from .api_asset_price_delegate import APIAssetPriceDelegate __all__ = [ PureMarketMakingASStrategy, - AssetPriceDelegate, - OrderBookAssetPriceDelegate, - APIAssetPriceDelegate, ] diff --git a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd deleted file mode 100644 index c37fb04d40..0000000000 --- a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pxd +++ /dev/null @@ -1,4 +0,0 @@ -from .asset_price_delegate cimport AssetPriceDelegate - -cdef class APIAssetPriceDelegate(AssetPriceDelegate): - cdef object _custom_api_feed diff --git a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx deleted file mode 100644 index 5134db639e..0000000000 --- a/hummingbot/strategy/pure_market_making_as/api_asset_price_delegate.pyx +++ /dev/null @@ -1,19 +0,0 @@ -from .asset_price_delegate cimport AssetPriceDelegate -from hummingbot.data_feed.custom_api_data_feed import CustomAPIDataFeed, NetworkStatus - -cdef class APIAssetPriceDelegate(AssetPriceDelegate): - def __init__(self, api_url: str): - super().__init__() - self._custom_api_feed = CustomAPIDataFeed(api_url=api_url) - self._custom_api_feed.start() - - cdef object c_get_mid_price(self): - return self._custom_api_feed.get_price() - - @property - def ready(self) -> bool: - return self._custom_api_feed.network_status == NetworkStatus.CONNECTED - - @property - def custom_api_feed(self) -> CustomAPIDataFeed: - return self._custom_api_feed diff --git a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd deleted file mode 100644 index af6a7bf0fd..0000000000 --- a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pxd +++ /dev/null @@ -1,3 +0,0 @@ - -cdef class AssetPriceDelegate: - cdef object c_get_mid_price(self) diff --git a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx deleted file mode 100644 index c68f3d665f..0000000000 --- a/hummingbot/strategy/pure_market_making_as/asset_price_delegate.pyx +++ /dev/null @@ -1,16 +0,0 @@ -from decimal import Decimal - - -cdef class AssetPriceDelegate: - # The following exposed Python functions are meant for unit tests - # --------------------------------------------------------------- - def get_mid_price(self) -> Decimal: - return self.c_get_mid_price() - # --------------------------------------------------------------- - - cdef object c_get_mid_price(self): - raise NotImplementedError - - @property - def ready(self) -> bool: - raise NotImplementedError diff --git a/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd deleted file mode 100644 index e787cf878c..0000000000 --- a/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pxd +++ /dev/null @@ -1,7 +0,0 @@ -from .asset_price_delegate cimport AssetPriceDelegate -from hummingbot.connector.exchange_base cimport ExchangeBase - -cdef class OrderBookAssetPriceDelegate(AssetPriceDelegate): - cdef: - ExchangeBase _market - str _trading_pair diff --git a/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx b/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx deleted file mode 100644 index 0383401698..0000000000 --- a/hummingbot/strategy/pure_market_making_as/order_book_asset_price_delegate.pyx +++ /dev/null @@ -1,29 +0,0 @@ -from hummingbot.core.event.events import PriceType -from .asset_price_delegate cimport AssetPriceDelegate -from hummingbot.connector.exchange_base import ExchangeBase -from decimal import Decimal - -cdef class OrderBookAssetPriceDelegate(AssetPriceDelegate): - def __init__(self, market: ExchangeBase, trading_pair: str): - super().__init__() - self._market = market - self._trading_pair = trading_pair - - cdef object c_get_mid_price(self): - return (self._market.c_get_price(self._trading_pair, True) + - self._market.c_get_price(self._trading_pair, False))/Decimal('2') - - @property - def ready(self) -> bool: - return self._market.ready - - def get_price_by_type(self, price_type: PriceType) -> Decimal: - return self._market.get_price_by_type(self._trading_pair, price_type) - - @property - def market(self) -> ExchangeBase: - return self._market - - @property - def trading_pair(self) -> str: - return self._trading_pair diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd index a95341feb7..4a32802d05 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd @@ -17,9 +17,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object _inventory_target_base_pct bint _order_optimization_enabled bint _add_transaction_costs_to_orders - object _asset_price_delegate - object _inventory_cost_price_delegate - object _price_type bint _hb_app_notification double _cancel_timestamp @@ -73,4 +70,5 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef c_calculate_reserved_price_and_optimal_spread(self) cdef object c_calculate_target_inventory(self) cdef c_recalculate_parameters(self) + cdef object c_calculate_eta(self) cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx index 1cbc332a07..569a6d6418 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx @@ -13,7 +13,7 @@ from math import ( import time import os from hummingbot.core.clock cimport Clock -from hummingbot.core.event.events import TradeType, PriceType +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 @@ -30,16 +30,13 @@ from .data_types import ( PriceSize ) from .pure_market_making_as_order_tracker import PureMarketMakingASOrderTracker - -from .asset_price_delegate cimport AssetPriceDelegate -from .asset_price_delegate import AssetPriceDelegate -from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate 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 @@ -69,8 +66,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): filled_order_delay: float = 60.0, inventory_target_base_pct: Decimal = s_decimal_zero, add_transaction_costs_to_orders: bool = True, - asset_price_delegate: AssetPriceDelegate = None, - price_type: str = "mid_price", logging_options: int = OPTION_LOG_ALL, status_report_interval: float = 900, hb_app_notification: bool = False, @@ -82,7 +77,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): kappa: Decimal = Decimal("0.1"), gamma: Decimal = Decimal("0.5"), eta: Decimal = Decimal("0.005"), - closing_time: Decimal = Decimal("86400000"), + closing_time: Decimal = Decimal("1"), csv_path: str = '', buffer_size: int = 30, buffer_sampling_period: int = 60 @@ -98,8 +93,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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._asset_price_delegate = asset_price_delegate - self._price_type = self.get_price_type(price_type) self._hb_app_notification = hb_app_notification self._cancel_timestamp = 0 @@ -119,7 +112,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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(buffer_size, 3) + self._avg_vol = AverageVolatilityIndicator(buffer_size, 1) self._buffer_sampling_period = buffer_sampling_period self._last_sampling_timestamp = 0 self._kappa = kappa @@ -222,18 +215,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return self._market_info.trading_pair def get_price(self) -> float: - price_provider = self._asset_price_delegate or self._market_info - if self._price_type is PriceType.LastOwnTrade: - price = self._last_own_trade_price - elif self._price_type is PriceType.InventoryCost: - price = price_provider.get_price_by_type(PriceType.MidPrice) - else: - price = price_provider.get_price_by_type(self._price_type) - - if price.is_nan(): - price = price_provider.get_price_by_type(PriceType.MidPrice) - - return price + return self.get_mid_price() def get_last_price(self) -> float: return self._market_info.get_last_price() @@ -242,14 +224,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): return self.c_get_mid_price() cdef object c_get_mid_price(self): - cdef: - AssetPriceDelegate delegate = self._asset_price_delegate - object mid_price - if self._asset_price_delegate is not None: - mid_price = delegate.c_get_mid_price() - else: - mid_price = self._market_info.get_mid_price() - return mid_price + return self._market_info.get_mid_price() @property def market_info_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: @@ -277,14 +252,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): def logging_options(self, int64_t logging_options): self._logging_options = logging_options - @property - def asset_price_delegate(self) -> AssetPriceDelegate: - return self._asset_price_delegate - - @asset_price_delegate.setter - def asset_price_delegate(self, value): - self._asset_price_delegate = value - @property def order_tracker(self): return self._sb_order_tracker @@ -343,25 +310,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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"Ref Price ({self._price_type.name})"] - if self._price_type is PriceType.LastOwnTrade and self._last_own_trade_price.is_nan(): - markets_columns[-1] = "Ref Price (MidPrice)" + markets_columns = ["Exchange", "Market", "Best Bid", "Best Ask", f"Ref Price (MidPrice)"] markets_columns.append('Reserved Price') market_books = [(self._market_info.market, self._market_info.trading_pair)] - if type(self._asset_price_delegate) is OrderBookAssetPriceDelegate: - market_books.append((self._asset_price_delegate.market, self._asset_price_delegate.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 = float("nan") - if market == self._market_info.market and self._asset_price_delegate is None: - ref_price = self.get_price() - elif ( - self._asset_price_delegate is not None - and market == self._asset_price_delegate.market - and self._price_type is not PriceType.LastOwnTrade - ): - ref_price = self._asset_price_delegate.get_price_by_type(self._price_type) + ref_price = self.get_price() markets_data.append([ market.display_name, trading_pair, @@ -434,8 +389,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): try: if not self._all_markets_ready: self._all_markets_ready = all([mkt.ready for mkt in self._sb_markets]) - if self._asset_price_delegate is not None and self._all_markets_ready: - self._all_markets_ready = self._asset_price_delegate.ready if not self._all_markets_ready: # Markets not ready yet. Don't do anything. if should_report_warnings: @@ -576,12 +529,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) # Want the maximum possible spread but with restrictions to avoid negative kappa or division by 0 - max_spread_around_reserved_price = max_spread + min_spread + 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('Inf') else: self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) + self._eta = self.c_calculate_eta() + self._latest_parameter_calculation_vol = vol cdef bint c_is_algorithm_ready(self): @@ -970,19 +925,16 @@ cdef class PureMarketMakingASStrategy(StrategyBase): from hummingbot.client.hummingbot_application import HummingbotApplication HummingbotApplication.main_application()._notify(msg) - def get_price_type(self, price_type_str: str) -> PriceType: - if price_type_str == "mid_price": - return PriceType.MidPrice - elif price_type_str == "best_bid": - return PriceType.BestBid - elif price_type_str == "best_ask": - return PriceType.BestAsk - elif price_type_str == "last_price": - return PriceType.LastTrade - elif price_type_str == 'last_own_trade_price': - return PriceType.LastOwnTrade + cdef c_calculate_eta(self): + cdef: + object total_inventory_in_base + object q_where_to_decay_order_amount + if not self._parameters_based_on_spread: + return self._eta else: - raise ValueError(f"Unrecognized price type string {price_type_str}.") + total_inventory_in_base = self.c_calculate_target_inventory() / self._inventory_target_base_pct + q_where_to_decay_order_amount = total_inventory_in_base * (1 - self._inventory_risk_aversion) + return s_decimal_one / q_where_to_decay_order_amount def dump_debug_variables(self): market = self._market_info.market diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index 907ae13210..1f1a21d175 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -53,46 +53,22 @@ def validate_order_amount(value: str) -> Optional[str]: return "Invalid order amount." -def validate_price_source(value: str) -> Optional[str]: - if value not in {"current_market", "external_market", "custom_api"}: - return "Invalid price source type." - - -def on_validate_price_source(value: str): - if value != "external_market": - pure_market_making_as_config_map["price_source_exchange"].value = None - pure_market_making_as_config_map["price_source_market"].value = None - if value != "custom_api": - pure_market_making_as_config_map["price_source_custom_api"].value = None - else: - pure_market_making_as_config_map["price_type"].value = None - - -def price_source_market_prompt() -> str: - external_market = pure_market_making_as_config_map.get("price_source_exchange").value - return f'Enter the token trading pair on {external_market} >>> ' - - -def validate_price_source_exchange(value: str) -> Optional[str]: - if value == pure_market_making_as_config_map.get("exchange").value: - return "Price source exchange cannot be the same as maker exchange." - return validate_exchange(value) - - def on_validated_price_source_exchange(value: str): if value is None: pure_market_making_as_config_map["price_source_market"].value = None -def validate_price_source_market(value: str) -> Optional[str]: - market = pure_market_making_as_config_map.get("price_source_exchange").value - return validate_market_trading_pair(market, value) - - def exchange_on_validated(value: str): required_exchanges.append(value) +def on_validated_parameters_based_on_spread(value: str): + if value == 'True': + pure_market_making_as_config_map.get("gamma").value = None + pure_market_making_as_config_map.get("kappa").value = None + pure_market_making_as_config_map.get("eta").value = None + + pure_market_making_as_config_map = { "strategy": ConfigVar(key="strategy", @@ -126,6 +102,7 @@ def exchange_on_validated(value: str): 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), "min_spread": ConfigVar(key="min_spread", @@ -233,45 +210,6 @@ def exchange_on_validated(value: str): type_str="bool", default=False, validator=validate_bool), - "price_source": - ConfigVar(key="price_source", - prompt="Which price source to use? (current_market/external_market/custom_api) >>> ", - type_str="str", - default="current_market", - validator=validate_price_source, - on_validated=on_validate_price_source), - "price_type": - ConfigVar(key="price_type", - prompt="Which price type to use? (" - "mid_price/last_price/last_own_trade_price/best_bid/best_ask) >>> ", - type_str="str", - required_if=lambda: pure_market_making_as_config_map.get("price_source").value != "custom_api", - default="mid_price", - validator=lambda s: None if s in {"mid_price", - "last_price", - "last_own_trade_price", - "best_bid", - "best_ask", - } else - "Invalid price type."), - "price_source_exchange": - ConfigVar(key="price_source_exchange", - prompt="Enter external price source exchange name >>> ", - required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "external_market", - type_str="str", - validator=validate_price_source_exchange, - on_validated=on_validated_price_source_exchange), - "price_source_market": - ConfigVar(key="price_source_market", - prompt=price_source_market_prompt, - required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "external_market", - type_str="str", - validator=validate_price_source_market), - "price_source_custom_api": - ConfigVar(key="price_source_custom_api", - prompt="Enter pricing API URL >>> ", - required_if=lambda: pure_market_making_as_config_map.get("price_source").value == "custom_api", - type_str="str"), "buffer_size": ConfigVar(key="buffer_size", prompt="Enter amount of samples to use for volatility calculation>>> ", diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/pure_market_making_as/start.py index f0be0dfe09..ba491b3fcc 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/pure_market_making_as/start.py @@ -9,12 +9,8 @@ from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple from hummingbot.strategy.pure_market_making_as import ( PureMarketMakingASStrategy, - OrderBookAssetPriceDelegate, - APIAssetPriceDelegate, ) from hummingbot.strategy.pure_market_making_as.pure_market_making_as_config_map import pure_market_making_as_config_map as c_map -from hummingbot.connector.exchange.paper_trade import create_paper_trade_market -from hummingbot.connector.exchange_base import ExchangeBase from decimal import Decimal import pandas as pd @@ -28,11 +24,6 @@ def start(self): 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') - price_source = c_map.get("price_source").value - price_type = c_map.get("price_type").value - price_source_exchange = c_map.get("price_source_exchange").value - price_source_market = c_map.get("price_source_market").value - price_source_custom_api = c_map.get("price_source_custom_api").value 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 @@ -45,14 +36,6 @@ def start(self): self.assets = set(maker_assets) maker_data = [self.markets[exchange], trading_pair] + list(maker_assets) self.market_trading_pair_tuples = [MarketTradingPairTuple(*maker_data)] - asset_price_delegate = None - if price_source == "external_market": - asset_trading_pair: str = price_source_market - ext_market = create_paper_trade_market(price_source_exchange, [asset_trading_pair]) - self.markets[price_source_exchange]: ExchangeBase = ext_market - asset_price_delegate = OrderBookAssetPriceDelegate(ext_market, asset_trading_pair) - elif price_source == "custom_api": - asset_price_delegate = APIAssetPriceDelegate(price_source_custom_api) strategy_logging_options = PureMarketMakingASStrategy.OPTION_LOG_ALL parameters_based_on_spread = c_map.get("parameters_based_on_spread").value @@ -61,10 +44,11 @@ def start(self): vol_to_spread_multiplier = c_map.get("vol_to_spread_multiplier").value inventory_risk_aversion = c_map.get("inventory_risk_aversion").value if parameters_based_on_spread: - gamma = kappa = -1 + gamma = kappa = eta = -1 else: kappa = c_map.get("kappa").value gamma = c_map.get("gamma").value + eta = c_map.get("eta").value closing_time = c_map.get("closing_time").value * Decimal(3600 * 24 * 1e3) buffer_size = c_map.get("buffer_size").value buffer_sampling_period = c_map.get("buffer_sampling_period").value @@ -82,8 +66,6 @@ def start(self): filled_order_delay=filled_order_delay, add_transaction_costs_to_orders=add_transaction_costs_to_orders, logging_options=strategy_logging_options, - asset_price_delegate=asset_price_delegate, - price_type=price_type, hb_app_notification=True, parameters_based_on_spread=parameters_based_on_spread, min_spread=min_spread, @@ -92,6 +74,7 @@ def start(self): inventory_risk_aversion = inventory_risk_aversion, kappa=kappa, gamma=gamma, + eta=eta, closing_time=closing_time, csv_path=csv_path, buffer_size=buffer_size, diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml index f85ec0953f..eeb2781004 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml @@ -37,21 +37,6 @@ inventory_target_base_pct: null # Whether to enable adding transaction costs to order price calculation (true/false). add_transaction_costs: null -# The price source (current_market/external_market/custom_api). -price_source: null - -# The price type (mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost). -price_type: null - -# An external exchange name (for external exchange pricing source). -price_source_exchange: null - -# A trading pair for the external exchange, e.g. BTC-USDT (for external exchange pricing source). -price_source_market: null - -# An external api that returns price (for custom_api pricing source). -price_source_custom_api: null - # Avellaneda - Stoikov algorithm parameters parameters_based_on_spread: null min_spread: null @@ -60,7 +45,7 @@ vol_to_spread_multiplier: null inventory_risk_aversion: null kappa: null gamma: null -eta: 0.005 +eta: null closing_time: null # Buffer size used to store historic samples and calculate volatility From e203467c0d1cd6b9f557118b5be6ccab3af6a3a8 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 17 Mar 2021 22:24:19 -0300 Subject: [PATCH 31/77] Added async calls to order_amount prompt and validations based on Jack's change --- .../pure_market_making_as_config_map.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py index 1f1a21d175..f0a8b060c8 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py @@ -34,19 +34,19 @@ def validate_exchange_trading_pair(value: str) -> Optional[str]: return validate_market_trading_pair(exchange, value) -def order_amount_prompt() -> str: +async def order_amount_prompt() -> str: exchange = pure_market_making_as_config_map["exchange"].value trading_pair = pure_market_making_as_config_map["market"].value base_asset, quote_asset = trading_pair.split("-") - min_amount = minimum_order_amount(exchange, trading_pair) + min_amount = await minimum_order_amount(exchange, trading_pair) return f"What is the amount of {base_asset} per order? (minimum {min_amount}) >>> " -def validate_order_amount(value: str) -> Optional[str]: +async def validate_order_amount(value: str) -> Optional[str]: try: exchange = pure_market_making_as_config_map["exchange"].value trading_pair = pure_market_making_as_config_map["market"].value - min_amount = minimum_order_amount(exchange, trading_pair) + 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: @@ -156,7 +156,7 @@ def on_validated_parameters_based_on_spread(value: str): type_str="decimal", required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1, inclusive=True), - default=Decimal("0.005")), + prompt_on_new=True), "closing_time": ConfigVar(key="closing_time", prompt="Enter algorithm closing time in days. " From 78709b1406e828f69a812cef873da00dc523356a Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 18 Mar 2021 18:26:02 -0300 Subject: [PATCH 32/77] Renamed strategy to fieldfare_mm --- hummingbot/strategy/fieldfare_mm/__init__.py | 6 +++ .../data_types.py | 0 .../fieldfare_mm.pxd} | 8 ++- .../fieldfare_mm.pyx} | 47 ++++++++---------- .../fieldfare_mm_config_map.py} | 41 ++++++++-------- .../start.py | 18 +++---- .../pure_market_making_as/__init__.py | 6 --- .../pure_market_making_as_order_tracker.pxd | 8 --- .../pure_market_making_as_order_tracker.pyx | 49 ------------------- ...> conf_fieldfare_mm_strategy_TEMPLATE.yml} | 5 +- 10 files changed, 61 insertions(+), 127 deletions(-) create mode 100644 hummingbot/strategy/fieldfare_mm/__init__.py rename hummingbot/strategy/{pure_market_making_as => fieldfare_mm}/data_types.py (100%) rename hummingbot/strategy/{pure_market_making_as/pure_market_making_as.pxd => fieldfare_mm/fieldfare_mm.pxd} (93%) rename hummingbot/strategy/{pure_market_making_as/pure_market_making_as.pyx => fieldfare_mm/fieldfare_mm.pyx} (97%) rename hummingbot/strategy/{pure_market_making_as/pure_market_making_as_config_map.py => fieldfare_mm/fieldfare_mm_config_map.py} (85%) rename hummingbot/strategy/{pure_market_making_as => fieldfare_mm}/start.py (85%) delete mode 100644 hummingbot/strategy/pure_market_making_as/__init__.py delete mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd delete mode 100644 hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx rename hummingbot/templates/{conf_pure_market_making_as_strategy_TEMPLATE.yml => conf_fieldfare_mm_strategy_TEMPLATE.yml} (89%) diff --git a/hummingbot/strategy/fieldfare_mm/__init__.py b/hummingbot/strategy/fieldfare_mm/__init__.py new file mode 100644 index 0000000000..bc05ca2af4 --- /dev/null +++ b/hummingbot/strategy/fieldfare_mm/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +from .fieldfare_mm import FieldfareMMStrategy +__all__ = [ + FieldfareMMStrategy, +] diff --git a/hummingbot/strategy/pure_market_making_as/data_types.py b/hummingbot/strategy/fieldfare_mm/data_types.py similarity index 100% rename from hummingbot/strategy/pure_market_making_as/data_types.py rename to hummingbot/strategy/fieldfare_mm/data_types.py diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd similarity index 93% rename from hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd rename to hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd index 4a32802d05..d1cb0fb386 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pxd +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd @@ -5,7 +5,7 @@ from hummingbot.strategy.strategy_base cimport StrategyBase from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator -cdef class PureMarketMakingASStrategy(StrategyBase): +cdef class FieldfareMMStrategy(StrategyBase): cdef: object _market_info object _minimum_spread @@ -46,16 +46,15 @@ cdef class PureMarketMakingASStrategy(StrategyBase): object _optimal_bid object _optimal_ask double _latest_parameter_calculation_vol - str _csv_path + 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_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) @@ -70,5 +69,4 @@ cdef class PureMarketMakingASStrategy(StrategyBase): cdef c_calculate_reserved_price_and_optimal_spread(self) cdef object c_calculate_target_inventory(self) cdef c_recalculate_parameters(self) - cdef object c_calculate_eta(self) cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx similarity index 97% rename from hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx rename to hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx index 569a6d6418..66f0936253 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as.pyx +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx @@ -29,7 +29,7 @@ from .data_types import ( Proposal, PriceSize ) -from .pure_market_making_as_order_tracker import PureMarketMakingASOrderTracker +from ..order_tracker cimport OrderTracker from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator @@ -40,7 +40,7 @@ s_decimal_one = Decimal(1) pmm_logger = None -cdef class PureMarketMakingASStrategy(StrategyBase): +cdef class FieldfareMMStrategy(StrategyBase): OPTION_LOG_CREATE_ORDER = 1 << 3 OPTION_LOG_MAKER_ORDER_FILLED = 1 << 4 OPTION_LOG_STATUS_REPORT = 1 << 5 @@ -78,12 +78,12 @@ cdef class PureMarketMakingASStrategy(StrategyBase): gamma: Decimal = Decimal("0.5"), eta: Decimal = Decimal("0.005"), closing_time: Decimal = Decimal("1"), - csv_path: str = '', + debug_csv_path: str = '', buffer_size: int = 30, buffer_sampling_period: int = 60 ): super().__init__() - self._sb_order_tracker = PureMarketMakingASOrderTracker() + self._sb_order_tracker = OrderTracker() self._market_info = market_info self._order_amount = order_amount self._order_optimization_enabled = order_optimization_enabled @@ -125,9 +125,9 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._optimal_spread = s_decimal_zero self._optimal_ask = s_decimal_zero self._optimal_bid = s_decimal_zero - self._csv_path = csv_path + self._debug_csv_path = debug_csv_path try: - os.unlink(self._csv_path) + os.unlink(self._debug_csv_path) except FileNotFoundError: pass @@ -410,20 +410,20 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > (self._vol_to_spread_multiplier - 1)): self.c_recalculate_parameters() self.c_calculate_reserved_price_and_optimal_spread() - self.dump_debug_variables() 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_modifiers(proposal) + 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) + self.dump_debug_variables() refresh_proposal = self.c_aged_order_refresh() # Firstly restore cancelled aged order if refresh_proposal is not None: @@ -521,13 +521,14 @@ cdef class PureMarketMakingASStrategy(StrategyBase): price=self.get_price() if vol > 0 and q != 0: - # Initially min_spread and max_spread defined by user will be used, but both of them will be modified by vol_to_spread_multiplier if vol too big min_spread = self._min_spread * price max_spread = self._max_spread * price - # If volatility is too high, gamma -> 0. Is this desirable? + # GAMMA + # If q or vol are close to 0, gamma will -> Inf. Is this desirable? self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) + # 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): @@ -535,7 +536,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): else: self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) - self._eta = self.c_calculate_eta() + # ETA + total_inventory_in_base = self.c_calculate_target_inventory() / self._inventory_target_base_pct + q_where_to_decay_order_amount = total_inventory_in_base * (1 - self._inventory_risk_aversion) + self._eta = s_decimal_one / q_where_to_decay_order_amount self._latest_parameter_calculation_vol = vol @@ -677,7 +681,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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_modifiers(self, object proposal): + 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 @@ -925,17 +929,6 @@ cdef class PureMarketMakingASStrategy(StrategyBase): from hummingbot.client.hummingbot_application import HummingbotApplication HummingbotApplication.main_application()._notify(msg) - cdef c_calculate_eta(self): - cdef: - object total_inventory_in_base - object q_where_to_decay_order_amount - if not self._parameters_based_on_spread: - return self._eta - else: - total_inventory_in_base = self.c_calculate_target_inventory() / self._inventory_target_base_pct - q_where_to_decay_order_amount = total_inventory_in_base * (1 - self._inventory_risk_aversion) - return s_decimal_one / q_where_to_decay_order_amount - def dump_debug_variables(self): market = self._market_info.market mid_price = self.get_price() @@ -945,7 +938,7 @@ cdef class PureMarketMakingASStrategy(StrategyBase): 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._csv_path): + if not os.path.exists(self._debug_csv_path): df_header = pd.DataFrame([('mid_price', 'spread', 'reserved_price', @@ -960,12 +953,13 @@ cdef class PureMarketMakingASStrategy(StrategyBase): '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._csv_path, mode='a', header=False, index=False) + df_header.to_csv(self._debug_csv_path, mode='a', header=False, index=False) df = pd.DataFrame([(mid_price, spread, self._reserved_price, @@ -980,9 +974,10 @@ cdef class PureMarketMakingASStrategy(StrategyBase): self._avg_vol.current_value, self._gamma, self._kappa, + self._eta, self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value), self.inventory_target_base_pct, self._min_spread, self._max_spread, self._vol_to_spread_multiplier)]) - df.to_csv(self._csv_path, mode='a', header=False, index=False) + df.to_csv(self._debug_csv_path, mode='a', header=False, index=False) diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py similarity index 85% rename from hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py rename to hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py index f0a8b060c8..60fb148993 100644 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_config_map.py +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py @@ -22,7 +22,7 @@ def maker_trading_pair_prompt(): - exchange = pure_market_making_as_config_map.get("exchange").value + exchange = fieldfare_mm_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 "") @@ -30,13 +30,13 @@ def maker_trading_pair_prompt(): # strategy specific validators def validate_exchange_trading_pair(value: str) -> Optional[str]: - exchange = pure_market_making_as_config_map.get("exchange").value + exchange = fieldfare_mm_config_map.get("exchange").value return validate_market_trading_pair(exchange, value) async def order_amount_prompt() -> str: - exchange = pure_market_making_as_config_map["exchange"].value - trading_pair = pure_market_making_as_config_map["market"].value + exchange = fieldfare_mm_config_map["exchange"].value + trading_pair = fieldfare_mm_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}) >>> " @@ -44,8 +44,8 @@ async def order_amount_prompt() -> str: async def validate_order_amount(value: str) -> Optional[str]: try: - exchange = pure_market_making_as_config_map["exchange"].value - trading_pair = pure_market_making_as_config_map["market"].value + exchange = fieldfare_mm_config_map["exchange"].value + trading_pair = fieldfare_mm_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}." @@ -55,7 +55,7 @@ async def validate_order_amount(value: str) -> Optional[str]: def on_validated_price_source_exchange(value: str): if value is None: - pure_market_making_as_config_map["price_source_market"].value = None + fieldfare_mm_config_map["price_source_market"].value = None def exchange_on_validated(value: str): @@ -64,16 +64,16 @@ def exchange_on_validated(value: str): def on_validated_parameters_based_on_spread(value: str): if value == 'True': - pure_market_making_as_config_map.get("gamma").value = None - pure_market_making_as_config_map.get("kappa").value = None - pure_market_making_as_config_map.get("eta").value = None + fieldfare_mm_config_map.get("gamma").value = None + fieldfare_mm_config_map.get("kappa").value = None + fieldfare_mm_config_map.get("eta").value = None -pure_market_making_as_config_map = { +fieldfare_mm_config_map = { "strategy": ConfigVar(key="strategy", prompt=None, - default="pure_market_making_as"), + default="fieldfare_mm"), "exchange": ConfigVar(key="exchange", prompt="Enter your maker exchange name >>> ", @@ -103,13 +103,14 @@ def on_validated_parameters_based_on_spread(value: str): type_str="bool", validator=validate_bool, on_validated=on_validated_parameters_based_on_spread, - default=True), + 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: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), prompt_on_new=True), "max_spread": @@ -117,7 +118,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the maximum spread allowed from mid-price in percentage " "(Enter 1 to indicate 1%) >>> ", type_str="decimal", - required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), prompt_on_new=True), "vol_to_spread_multiplier": @@ -125,7 +126,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the Volatility-to-Spread multiplier: " "Beyond this number of sigmas, spreads will turn into multiples of volatility >>>", type_str="decimal", - required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), prompt_on_new=True), "inventory_risk_aversion": @@ -133,28 +134,28 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter Inventory risk aversion: With 1.0 being extremely conservative about meeting inventory target, " "at the expense of profit, and 0.0 for a profit driven, at the expense of inventory risk >>>", type_str="decimal", - required_if=lambda: pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), prompt_on_new=True), "kappa": ConfigVar(key="kappa", prompt="Enter order book depth variable (kappa) >>> ", type_str="decimal", - required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "gamma": ConfigVar(key="gamma", prompt="Enter risk factor (gamma) >>> ", type_str="decimal", - required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), "eta": ConfigVar(key="eta", prompt="Enter order amount shape factor (eta) >>> ", type_str="decimal", - required_if=lambda: not pure_market_making_as_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1, inclusive=True), prompt_on_new=True), "closing_time": diff --git a/hummingbot/strategy/pure_market_making_as/start.py b/hummingbot/strategy/fieldfare_mm/start.py similarity index 85% rename from hummingbot/strategy/pure_market_making_as/start.py rename to hummingbot/strategy/fieldfare_mm/start.py index ba491b3fcc..7a8b860a49 100644 --- a/hummingbot/strategy/pure_market_making_as/start.py +++ b/hummingbot/strategy/fieldfare_mm/start.py @@ -7,10 +7,10 @@ import os.path from hummingbot.client.hummingbot_application import HummingbotApplication from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple -from hummingbot.strategy.pure_market_making_as import ( - PureMarketMakingASStrategy, +from hummingbot.strategy.fieldfare_mm import ( + FieldfareMMStrategy, ) -from hummingbot.strategy.pure_market_making_as.pure_market_making_as_config_map import pure_market_making_as_config_map as c_map +from hummingbot.strategy.fieldfare_mm.fieldfare_mm_config_map import fieldfare_mm_config_map as c_map from decimal import Decimal import pandas as pd @@ -37,7 +37,7 @@ def start(self): maker_data = [self.markets[exchange], trading_pair] + list(maker_assets) self.market_trading_pair_tuples = [MarketTradingPairTuple(*maker_data)] - strategy_logging_options = PureMarketMakingASStrategy.OPTION_LOG_ALL + strategy_logging_options = FieldfareMMStrategy.OPTION_LOG_ALL parameters_based_on_spread = c_map.get("parameters_based_on_spread").value min_spread = c_map.get("min_spread").value / Decimal(100) max_spread = c_map.get("max_spread").value / Decimal(100) @@ -52,11 +52,11 @@ def start(self): closing_time = c_map.get("closing_time").value * Decimal(3600 * 24 * 1e3) buffer_size = c_map.get("buffer_size").value buffer_sampling_period = c_map.get("buffer_sampling_period").value - 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") + 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 = PureMarketMakingASStrategy( + self.strategy = FieldfareMMStrategy( market_info=MarketTradingPairTuple(*maker_data), order_amount=order_amount, order_optimization_enabled=order_optimization_enabled, @@ -76,7 +76,7 @@ def start(self): gamma=gamma, eta=eta, closing_time=closing_time, - csv_path=csv_path, + debug_csv_path=debug_csv_path, buffer_size=buffer_size, buffer_sampling_period=buffer_sampling_period, ) diff --git a/hummingbot/strategy/pure_market_making_as/__init__.py b/hummingbot/strategy/pure_market_making_as/__init__.py deleted file mode 100644 index 337540fe0f..0000000000 --- a/hummingbot/strategy/pure_market_making_as/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -from .pure_market_making_as import PureMarketMakingASStrategy -__all__ = [ - PureMarketMakingASStrategy, -] diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd deleted file mode 100644 index e25c74d225..0000000000 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pxd +++ /dev/null @@ -1,8 +0,0 @@ -# distutils: language=c++ - -from hummingbot.strategy.order_tracker import OrderTracker -from hummingbot.strategy.order_tracker cimport OrderTracker - - -cdef class PureMarketMakingOrderTracker(OrderTracker): - pass diff --git a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx b/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx deleted file mode 100644 index 1296b24b04..0000000000 --- a/hummingbot/strategy/pure_market_making_as/pure_market_making_as_order_tracker.pyx +++ /dev/null @@ -1,49 +0,0 @@ -from typing import ( - Dict, - List, - Tuple -) - -from hummingbot.core.data_type.limit_order cimport LimitOrder -from hummingbot.core.data_type.limit_order import LimitOrder -from hummingbot.connector.connector_base import ConnectorBase -from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple -from hummingbot.strategy.order_tracker cimport OrderTracker - -NaN = float("nan") - - -cdef class PureMarketMakingASOrderTracker(OrderTracker): - # ETH confirmation requirement of Binance has shortened to 12 blocks as of 7/15/2019. - # 12 * 15 / 60 = 3 minutes - SHADOW_MAKER_ORDER_KEEP_ALIVE_DURATION = 60.0 * 3 - - def __init__(self): - super().__init__() - - @property - def active_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: - limit_orders = [] - for market_pair, orders_map in self._tracked_limit_orders.items(): - for limit_order in orders_map.values(): - limit_orders.append((market_pair.market, limit_order)) - return limit_orders - - @property - def shadow_limit_orders(self) -> List[Tuple[ConnectorBase, LimitOrder]]: - limit_orders = [] - for market_pair, orders_map in self._shadow_tracked_limit_orders.items(): - for limit_order in orders_map.values(): - limit_orders.append((market_pair.market, limit_order)) - return limit_orders - - @property - def market_pair_to_active_orders(self) -> Dict[MarketTradingPairTuple, List[LimitOrder]]: - market_pair_to_orders = {} - market_pairs = self._tracked_limit_orders.keys() - for market_pair in market_pairs: - maker_orders = [] - for limit_order in self._tracked_limit_orders[market_pair].values(): - maker_orders.append(limit_order) - market_pair_to_orders[market_pair] = maker_orders - return market_pair_to_orders diff --git a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml b/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml similarity index 89% rename from hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml rename to hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml index eeb2781004..d05fae4f24 100644 --- a/hummingbot/templates/conf_pure_market_making_as_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml @@ -1,5 +1,5 @@ ######################################################## -### Pure market making strategy config ### +### Fieldfare market making strategy config ### ######################################################## template_version: 1 @@ -51,6 +51,3 @@ closing_time: null # Buffer size used to store historic samples and calculate volatility buffer_size: 60 buffer_sampling_period: 1 - -# For more detailed information, see: -# https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters From fc2de05f5d72725e22ec967692a8356610847b33 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:07:15 +0800 Subject: [PATCH 33/77] (feat) add oracle configuration --- hummingbot/client/command/rate_command.py | 16 ++++++--- hummingbot/client/command/start_command.py | 34 +++++++++++++++++-- hummingbot/client/errors.py | 4 +++ hummingbot/client/settings.py | 3 ++ .../arbitrage/arbitrage_config_map.py | 25 +++++++++++++- .../conf_arbitrage_strategy_TEMPLATE.yml | 6 +++- 6 files changed, 80 insertions(+), 8 deletions(-) diff --git a/hummingbot/client/command/rate_command.py b/hummingbot/client/command/rate_command.py index a23135c0dd..ad162e2928 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 = 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 e8c2e589a3..2174d463fe 100644 --- a/hummingbot/client/command/start_command.py +++ b/hummingbot/client/command/start_command.py @@ -23,6 +23,7 @@ ethereum_gas_station_required, 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 @@ -30,6 +31,9 @@ from hummingbot.core.utils.eth_gas_station_lookup import EthGasStationLookup 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 if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -59,15 +63,16 @@ 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()): + return 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, @@ -155,3 +160,28 @@ 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_conversion_rate", + type_str="bool", + prompt="Please confirm 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 + finally: + self.placeholder_mode = False + self.app.hide_input = False + self.app.change_prompt(prompt=">>> ") + return result 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/strategy/arbitrage/arbitrage_config_map.py b/hummingbot/strategy/arbitrage/arbitrage_config_map.py index 96313098ae..24da734699 100644 --- a/hummingbot/strategy/arbitrage/arbitrage_config_map.py +++ b/hummingbot/strategy/arbitrage/arbitrage_config_map.py @@ -2,12 +2,14 @@ from hummingbot.client.config.config_validators import ( validate_exchange, validate_market_trading_pair, - validate_decimal + validate_decimal, + validate_bool ) from hummingbot.client.settings import ( required_exchanges, EXAMPLE_PAIRS, ) +import hummingbot.client.settings as settings from decimal import Decimal from typing import Optional @@ -40,6 +42,19 @@ def secondary_market_on_validated(value: str): required_exchanges.append(value) +def use_oracle_conversion_rate_on_validated(value: bool): + # global required_rate_oracle, rate_oracle_pairs + first_base, first_quote = arbitrage_config_map["primary_market_trading_pair"].value.split("-") + second_base, second_quote = arbitrage_config_map["secondary_market_trading_pair"].value.split("-") + if value 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}") + + arbitrage_config_map = { "strategy": ConfigVar(key="strategy", @@ -74,6 +89,14 @@ def secondary_market_on_validated(value: str): default=Decimal("0.3"), validator=lambda v: validate_decimal(v, Decimal(-100), Decimal("100"), inclusive=True), 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? >>> ", + prompt_on_new=True, + default=True, + validator=lambda v: validate_bool(v), + on_validated=use_oracle_conversion_rate_on_validated), "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. " 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) From 2f1e96a7e18f77f1a7906ab27a2f528044c5b6dd Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Tue, 23 Mar 2021 21:20:47 -0300 Subject: [PATCH 34/77] changed strategy parameters to descriptive names and added printable_key to be used in the listing of strategy config parameters --- hummingbot/client/command/config_command.py | 2 +- hummingbot/client/config/config_var.py | 4 ++- .../strategy/fieldfare_mm/fieldfare_mm.pyx | 18 +++++++----- .../fieldfare_mm/fieldfare_mm_config_map.py | 29 ++++++++++--------- hummingbot/strategy/fieldfare_mm/start.py | 16 +++++----- .../conf_fieldfare_mm_strategy_TEMPLATE.yml | 6 ++-- 6 files changed, 42 insertions(+), 33 deletions(-) diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 97e4abeab3..b4b9c0fc10 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -82,7 +82,7 @@ def list_configs(self, # type: HummingbotApplication self._notify("\n".join(lines)) if self.strategy_name is not None: - data = [[cv.key, cv.value] for cv in self.strategy_config_map.values() if not cv.is_secure] + data = [[cv.printable_key or cv.key, cv.value] for cv in self.strategy_config_map.values() if not cv.is_secure] df = pd.DataFrame(data=data, columns=columns) self._notify("\nStrategy Configurations:") lines = [" " + line for line in df.to_string(index=False, max_colwidth=50).split("\n")] diff --git a/hummingbot/client/config/config_var.py b/hummingbot/client/config/config_var.py index 85811bf2fb..6a346653b9 100644 --- a/hummingbot/client/config/config_var.py +++ b/hummingbot/client/config/config_var.py @@ -24,7 +24,8 @@ def __init__(self, # Whether to prompt a user for value when new strategy config file is created prompt_on_new: bool = False, # Whether this is a config var used in connect command - is_connect_key: bool = False): + is_connect_key: bool = False, + printable_key: str = None): self.prompt = prompt self.key = key self.value = None @@ -36,6 +37,7 @@ def __init__(self, self._on_validated = on_validated self.prompt_on_new = prompt_on_new self.is_connect_key = is_connect_key + self.printable_key = printable_key async def get_prompt(self): if inspect.iscoroutinefunction(self.prompt): diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx index 66f0936253..a106118352 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx @@ -74,9 +74,9 @@ cdef class FieldfareMMStrategy(StrategyBase): max_spread: Decimal = Decimal("2"), vol_to_spread_multiplier: Decimal = Decimal("1.3"), inventory_risk_aversion: Decimal = Decimal("0.5"), - kappa: Decimal = Decimal("0.1"), - gamma: Decimal = Decimal("0.5"), - eta: Decimal = Decimal("0.005"), + 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 = '', buffer_size: int = 30, @@ -115,9 +115,9 @@ cdef class FieldfareMMStrategy(StrategyBase): self._avg_vol = AverageVolatilityIndicator(buffer_size, 1) self._buffer_sampling_period = buffer_sampling_period self._last_sampling_timestamp = 0 - self._kappa = kappa - self._gamma = gamma - self._eta = eta + 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 = 0 @@ -352,7 +352,11 @@ cdef class FieldfareMMStrategy(StrategyBase): lines.extend(["", " No active maker orders."]) volatility_pct = self._avg_vol.current_value / float(self.get_price()) * 100.0 - lines.extend(["", f"Avellaneda-Stoikov: Gamma= {self._gamma:.5E} | Kappa= {self._kappa:.5E} | Volatility= {volatility_pct:.3f}% | Time left fraction= {self._time_left/self._closing_time:.4f}"]) + lines.extend(["", f" fieldfare_mm parameters:", + f" risk_factor(\u03B3)= {self._gamma:.5E}", + f" order_book_depth_factor(\u03BA)= {self._kappa:.5E}", + f" volatility= {volatility_pct:.3f}%", + f" time left fraction= {self._time_left/self._closing_time:.4f}"]) warning_lines.extend(self.balance_warning([self._market_info])) diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py index 60fb148993..bc4c0ce44b 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py @@ -64,9 +64,9 @@ def exchange_on_validated(value: str): def on_validated_parameters_based_on_spread(value: str): if value == 'True': - fieldfare_mm_config_map.get("gamma").value = None - fieldfare_mm_config_map.get("kappa").value = None - fieldfare_mm_config_map.get("eta").value = None + fieldfare_mm_config_map.get("risk_factor").value = None + fieldfare_mm_config_map.get("order_book_depth_factor").value = None + fieldfare_mm_config_map.get("order_amount_shape_factor").value = None fieldfare_mm_config_map = { @@ -137,23 +137,26 @@ def on_validated_parameters_based_on_spread(value: str): required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1, inclusive=False), prompt_on_new=True), - "kappa": - ConfigVar(key="kappa", - prompt="Enter order book depth variable (kappa) >>> ", + "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 fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), - "gamma": - ConfigVar(key="gamma", - prompt="Enter risk factor (gamma) >>> ", + "risk_factor": + ConfigVar(key="risk_factor", + printable_key="risk_factor(\u03B3)", + prompt="Enter risk factor (\u03B3) >>> ", type_str="decimal", required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1e10, inclusive=False), prompt_on_new=True), - "eta": - ConfigVar(key="eta", - prompt="Enter order amount shape factor (eta) >>> ", + "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 fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 1, inclusive=True), @@ -165,7 +168,7 @@ def on_validated_parameters_based_on_spread(value: str): " (fractional quantities are allowed i.e. 1.27 days) >>> ", type_str="decimal", validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), - default=Decimal("1")), + 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 " diff --git a/hummingbot/strategy/fieldfare_mm/start.py b/hummingbot/strategy/fieldfare_mm/start.py index 7a8b860a49..3b8e6f91e1 100644 --- a/hummingbot/strategy/fieldfare_mm/start.py +++ b/hummingbot/strategy/fieldfare_mm/start.py @@ -44,11 +44,11 @@ def start(self): vol_to_spread_multiplier = c_map.get("vol_to_spread_multiplier").value inventory_risk_aversion = c_map.get("inventory_risk_aversion").value if parameters_based_on_spread: - gamma = kappa = eta = -1 + risk_factor = order_book_depth_factor = order_amount_shape_factor = -1 else: - kappa = c_map.get("kappa").value - gamma = c_map.get("gamma").value - eta = c_map.get("eta").value + 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) buffer_size = c_map.get("buffer_size").value buffer_sampling_period = c_map.get("buffer_sampling_period").value @@ -71,10 +71,10 @@ def start(self): min_spread=min_spread, max_spread=max_spread, vol_to_spread_multiplier=vol_to_spread_multiplier, - inventory_risk_aversion = inventory_risk_aversion, - kappa=kappa, - gamma=gamma, - eta=eta, + 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, buffer_size=buffer_size, diff --git a/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml b/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml index d05fae4f24..4c02c2e7ae 100644 --- a/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml @@ -43,9 +43,9 @@ min_spread: null max_spread: null vol_to_spread_multiplier: null inventory_risk_aversion: null -kappa: null -gamma: null -eta: 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 From d98703e6d166e3e38d813a4bb1869f70165812b3 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Tue, 23 Mar 2021 21:22:41 -0300 Subject: [PATCH 35/77] Minor naming change --- hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx index a106118352..7852425d4e 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx @@ -352,7 +352,7 @@ cdef class FieldfareMMStrategy(StrategyBase): lines.extend(["", " No active maker orders."]) volatility_pct = self._avg_vol.current_value / float(self.get_price()) * 100.0 - lines.extend(["", f" fieldfare_mm parameters:", + 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}%", From 06645d1a68067bded124e40184b6e8c7153edd48 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 24 Mar 2021 02:55:52 -0300 Subject: [PATCH 36/77] Modified parameter input validation and switching from parameters_based_on_spread = True to False --- .../strategy/fieldfare_mm/fieldfare_mm.pyx | 24 +++++++++++-------- .../fieldfare_mm/fieldfare_mm_config_map.py | 24 +++++++++++++++++-- hummingbot/strategy/fieldfare_mm/start.py | 11 +++++---- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx index 7852425d4e..ea64e576ce 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx @@ -409,7 +409,7 @@ cdef class FieldfareMMStrategy(StrategyBase): # 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 == s_decimal_neg_one or self._kappa == s_decimal_neg_one) or \ + if (self._gamma is None) or (self._kappa is None) or \ (self._parameters_based_on_spread and self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > (self._vol_to_spread_multiplier - 1)): self.c_recalculate_parameters() @@ -473,14 +473,21 @@ cdef class FieldfareMMStrategy(StrategyBase): q = market.get_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory())) vol = Decimal(str(self._avg_vol.current_value)) mid_price_variance = vol ** 2 - self._reserved_price = price - (q * self._gamma * mid_price_variance * time_left_fraction) - 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) + 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_spread = self._gamma * mid_price_variance * time_left_fraction + 2 * Decimal(1 + self._gamma / self._kappa).ln() / self._gamma self._optimal_ask = min(max(self._reserved_price + self._optimal_spread / 2, min_limit_ask), max_limit_ask) @@ -491,9 +498,6 @@ cdef class FieldfareMMStrategy(StrategyBase): # Optimal bid and optimal ask prices will be used 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"vol_based_bid/ask={self._vol_to_spread_multiplier * vol / price * 100:.4f}% | " - f"opt_bid={(price-self._optimal_bid) / price * 100:.4f}% | " - f"opt_ask={(self._optimal_ask-price) / price * 100:.4f}% | " f"q={q:.4f} | " f"vol={vol:.4f}") diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py index bc4c0ce44b..a30691c486 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py @@ -34,6 +34,20 @@ def validate_exchange_trading_pair(value: str) -> Optional[str]: return validate_market_trading_pair(exchange, value) +def validate_max_spread(value: str) -> Optional[str]: + validate_decimal(value, 0, 100, inclusive=False) + if fieldfare_mm_config_map["min_spread"].value is not None: + min_spread = Decimal(fieldfare_mm_config_map["min_spread"].value) + max_spread = Decimal(value) + if min_spread > max_spread: + return f"Max spread cannot be lesser than 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 + fieldfare_mm_config_map["max_spread"].value = None + + async def order_amount_prompt() -> str: exchange = fieldfare_mm_config_map["exchange"].value trading_pair = fieldfare_mm_config_map["market"].value @@ -67,6 +81,11 @@ def on_validated_parameters_based_on_spread(value: str): fieldfare_mm_config_map.get("risk_factor").value = None fieldfare_mm_config_map.get("order_book_depth_factor").value = None fieldfare_mm_config_map.get("order_amount_shape_factor").value = None + else: + fieldfare_mm_config_map.get("max_spread").value = None + fieldfare_mm_config_map.get("min_spread").value = None + fieldfare_mm_config_map.get("vol_to_spread_multiplier").value = None + fieldfare_mm_config_map.get("inventory_risk_aversion").value = None fieldfare_mm_config_map = { @@ -112,14 +131,15 @@ def on_validated_parameters_based_on_spread(value: str): type_str="decimal", required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), - prompt_on_new=True), + 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: fieldfare_mm_config_map.get("parameters_based_on_spread").value, - validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + validator=lambda v: validate_max_spread(v), prompt_on_new=True), "vol_to_spread_multiplier": ConfigVar(key="vol_to_spread_multiplier", diff --git a/hummingbot/strategy/fieldfare_mm/start.py b/hummingbot/strategy/fieldfare_mm/start.py index 3b8e6f91e1..95557d3521 100644 --- a/hummingbot/strategy/fieldfare_mm/start.py +++ b/hummingbot/strategy/fieldfare_mm/start.py @@ -39,13 +39,14 @@ def start(self): strategy_logging_options = FieldfareMMStrategy.OPTION_LOG_ALL parameters_based_on_spread = c_map.get("parameters_based_on_spread").value - 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 if parameters_based_on_spread: - risk_factor = order_book_depth_factor = order_amount_shape_factor = -1 + 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 From b0de70a6c9f94319de5a33c04ca0e1874874c943 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:16:59 +0800 Subject: [PATCH 37/77] (feat) add oracle to arbitrage strategy --- hummingbot/client/command/rate_command.py | 2 +- hummingbot/client/command/start_command.py | 27 ++++++++++--------- hummingbot/client/command/stop_command.py | 4 +++ hummingbot/strategy/arbitrage/arbitrage.pxd | 1 + hummingbot/strategy/arbitrage/arbitrage.pyx | 13 +++++++-- .../arbitrage/arbitrage_config_map.py | 24 ++++++++--------- hummingbot/strategy/arbitrage/start.py | 2 ++ 7 files changed, 45 insertions(+), 28 deletions(-) diff --git a/hummingbot/client/command/rate_command.py b/hummingbot/client/command/rate_command.py index ad162e2928..b784e3c0c3 100644 --- a/hummingbot/client/command/rate_command.py +++ b/hummingbot/client/command/rate_command.py @@ -31,7 +31,7 @@ async def show_rate(self, # type: HummingbotApplication pair: str, ): try: - msg = RateCommand.oracle_rate_msg(pair) + msg = await RateCommand.oracle_rate_msg(pair) except OracleRateUnavailable: msg = "Rate is not available." self._notify(msg) diff --git a/hummingbot/client/command/start_command.py b/hummingbot/client/command/start_command.py index 2174d463fe..bbb0690972 100644 --- a/hummingbot/client/command/start_command.py +++ b/hummingbot/client/command/start_command.py @@ -17,12 +17,6 @@ from hummingbot.client.config.config_helpers import ( get_strategy_starter_file, ) -from hummingbot.client.settings import ( - STRATEGIES, - SCRIPTS_PATH, - ethereum_gas_station_required, - 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 @@ -34,6 +28,8 @@ 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 @@ -69,7 +65,10 @@ async def start_check(self, # type: HummingbotApplication 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 @@ -90,7 +89,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) @@ -111,7 +110,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 @@ -138,7 +137,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: @@ -147,7 +146,7 @@ async def start_market_making(self, # type: HummingbotApplication self.clock.add_iterator(self._script_iterator) self._notify(f"Script ({script_file}) started.") - if global_config_map["ethgasstation_gas_enabled"].value and ethereum_gas_station_required(): + if global_config_map["ethgasstation_gas_enabled"].value and settings.ethereum_gas_station_required(): EthGasStationLookup.get_instance().start() self.strategy_task: asyncio.Task = safe_ensure_future(self._run_clock(), loop=self.ev_loop) @@ -171,15 +170,17 @@ async def confirm_oracle_conversion_rate(self, # type: HummingbotApplication 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_conversion_rate", + config = ConfigVar(key="confirm_oracle_use", type_str="bool", - prompt="Please confirm if the above oracle source and rates are correct for this " - "strategy (Yes/No) >>> ", + 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 diff --git a/hummingbot/client/command/stop_command.py b/hummingbot/client/command/stop_command.py index 3848aadea4..bf5340f827 100644 --- a/hummingbot/client/command/stop_command.py +++ b/hummingbot/client/command/stop_command.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.core.utils.eth_gas_station_lookup import EthGasStationLookup +from hummingbot.core.rate_oracle.rate_oracle import RateOracle if TYPE_CHECKING: from hummingbot.client.hummingbot_application import HummingbotApplication @@ -45,6 +46,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 EthGasStationLookup.get_instance().started: EthGasStationLookup.get_instance().stop() diff --git a/hummingbot/strategy/arbitrage/arbitrage.pxd b/hummingbot/strategy/arbitrage/arbitrage.pxd index 316fd6ede6..56eed3b065 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pxd +++ b/hummingbot/strategy/arbitrage/arbitrage.pxd @@ -23,6 +23,7 @@ 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 diff --git a/hummingbot/strategy/arbitrage/arbitrage.pyx b/hummingbot/strategy/arbitrage/arbitrage.pyx index d08a6afc83..7090a2279f 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pyx +++ b/hummingbot/strategy/arbitrage/arbitrage.pyx @@ -20,6 +20,7 @@ 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 NaN = float("nan") s_decimal_0 = Decimal(0) @@ -49,6 +50,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,7 +77,7 @@ 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 @@ -390,7 +392,14 @@ 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 + if not self._use_oracle_conversion_rate: + return self._secondary_to_primary_quote_conversion_rate / self._secondary_to_primary_base_conversion_rate + else: + quote_pair = f"{self._market_pairs[0].second.quote_asset}-{self._market_pairs[0].first.quote_asset}" + base_pair = f"{self._market_pairs[0].second.base_asset}-{self._market_pairs[0].first.base_asset}" + quote_rate = RateOracle.get_instance().rate(quote_pair) + 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 24da734699..c903f8a022 100644 --- a/hummingbot/strategy/arbitrage/arbitrage_config_map.py +++ b/hummingbot/strategy/arbitrage/arbitrage_config_map.py @@ -5,10 +5,7 @@ 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 typing import Optional @@ -26,33 +23,37 @@ 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 use_oracle_conversion_rate_on_validated(value: bool): +def use_oracle_conversion_rate_on_validated(value: str): # global required_rate_oracle, rate_oracle_pairs + use_oracle = parse_cvar_value(arbitrage_config_map["use_oracle_conversion_rate"], value) first_base, first_quote = arbitrage_config_map["primary_market_trading_pair"].value.split("-") second_base, second_quote = arbitrage_config_map["secondary_market_trading_pair"].value.split("-") - if value and (first_base != second_base or first_quote != second_quote): + 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 = { @@ -65,7 +66,7 @@ def use_oracle_conversion_rate_on_validated(value: bool): 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 >>> ", @@ -92,9 +93,8 @@ def use_oracle_conversion_rate_on_validated(value: bool): "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? >>> ", + prompt="Do you want to use rate oracle on unmatched trading pairs? (Yes/No) >>> ", prompt_on_new=True, - default=True, validator=lambda v: validate_bool(v), on_validated=use_oracle_conversion_rate_on_validated), "secondary_to_primary_base_conversion_rate": ConfigVar( 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) From 177aca41e6878ef37686688d86a5c5b0ff7b8886 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 24 Mar 2021 11:34:35 -0300 Subject: [PATCH 38/77] Merged minor details from development branch --- hummingbot/VERSION | 2 +- hummingbot/client/config/config_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hummingbot/VERSION b/hummingbot/VERSION index 9b1bb85123..9a859936a4 100644 --- a/hummingbot/VERSION +++ b/hummingbot/VERSION @@ -1 +1 @@ -0.37.1 +dev-0.38.0 diff --git a/hummingbot/client/config/config_helpers.py b/hummingbot/client/config/config_helpers.py index ac9a8348d4..c700d0d486 100644 --- a/hummingbot/client/config/config_helpers.py +++ b/hummingbot/client/config/config_helpers.py @@ -171,7 +171,7 @@ def get_erc20_token_addresses() -> Dict[str, List]: address_file_path = TOKEN_ADDRESSES_FILE_PATH token_list = {} - resp = requests.get(token_list_url, timeout=1) + resp = requests.get(token_list_url, timeout=3) decoded_resp = resp.json() for token in decoded_resp["tokens"]: From 025201bf6b217b018dac0ea58644ca6df6099e7b Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 25 Mar 2021 14:23:42 -0300 Subject: [PATCH 39/77] Added q adjustment factor to scale when q is very small --- hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd | 1 + hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd index d1cb0fb386..607c6a74dd 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd @@ -41,6 +41,7 @@ cdef class FieldfareMMStrategy(StrategyBase): object _eta object _closing_time object _time_left + object _q_ajustment_factor object _reserved_price object _optimal_spread object _optimal_bid diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx index ea64e576ce..ed3944b95c 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx +++ b/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx @@ -120,6 +120,7 @@ cdef class FieldfareMMStrategy(StrategyBase): self._eta = order_amount_shape_factor self._time_left = closing_time self._closing_time = closing_time + self._q_ajustment_factor = Decimal("10")/self._order_amount self._latest_parameter_calculation_vol = 0 self._reserved_price = s_decimal_zero self._optimal_spread = s_decimal_zero @@ -470,7 +471,7 @@ cdef class FieldfareMMStrategy(StrategyBase): 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())) + q = (market.get_balance(self.base_asset) - Decimal(str(self.c_calculate_target_inventory()))) * self._q_ajustment_factor vol = Decimal(str(self._avg_vol.current_value)) mid_price_variance = vol ** 2 @@ -498,7 +499,7 @@ cdef class FieldfareMMStrategy(StrategyBase): # Optimal bid and optimal ask prices will be used 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:.4f} | " + f"q={q/self._q_ajustment_factor:.4f} | " f"vol={vol:.4f}") cdef object c_calculate_target_inventory(self): @@ -524,7 +525,7 @@ cdef class FieldfareMMStrategy(StrategyBase): cdef: ExchangeBase market = self._market_info.market - q = market.get_balance(self.base_asset) - self.c_calculate_target_inventory() + q = (market.get_balance(self.base_asset) - self.c_calculate_target_inventory()) * self._q_ajustment_factor vol = Decimal(str(self._avg_vol.current_value)) price=self.get_price() @@ -534,13 +535,17 @@ cdef class FieldfareMMStrategy(StrategyBase): # GAMMA # If q or vol are close to 0, gamma will -> Inf. Is this desirable? - self._gamma = self._inventory_risk_aversion * (max_spread - min_spread) / (2 * abs(q) * (vol ** 2)) + 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('Inf') + 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) From 42311afb5d95d076819f87409de50f66059b507b Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 25 Mar 2021 22:39:09 -0300 Subject: [PATCH 40/77] Added modifications suggested by Paulo --- .../fieldfare_market_making/__init__.py | 6 ++ .../data_types.py | 0 .../fieldfare_market_making.pxd} | 10 +-- .../fieldfare_market_making.pyx} | 60 ++++++++----- .../fieldfare_market_making_config_map.py} | 85 ++++++++++--------- .../start.py | 18 ++-- hummingbot/strategy/fieldfare_mm/__init__.py | 6 -- ...dfare_market_making_strategy_TEMPLATE.yml} | 4 +- 8 files changed, 103 insertions(+), 86 deletions(-) create mode 100644 hummingbot/strategy/fieldfare_market_making/__init__.py rename hummingbot/strategy/{fieldfare_mm => fieldfare_market_making}/data_types.py (100%) rename hummingbot/strategy/{fieldfare_mm/fieldfare_mm.pxd => fieldfare_market_making/fieldfare_market_making.pxd} (90%) rename hummingbot/strategy/{fieldfare_mm/fieldfare_mm.pyx => fieldfare_market_making/fieldfare_market_making.pyx} (95%) rename hummingbot/strategy/{fieldfare_mm/fieldfare_mm_config_map.py => fieldfare_market_making/fieldfare_market_making_config_map.py} (70%) rename hummingbot/strategy/{fieldfare_mm => fieldfare_market_making}/start.py (86%) delete mode 100644 hummingbot/strategy/fieldfare_mm/__init__.py rename hummingbot/templates/{conf_fieldfare_mm_strategy_TEMPLATE.yml => conf_fieldfare_market_making_strategy_TEMPLATE.yml} (96%) diff --git a/hummingbot/strategy/fieldfare_market_making/__init__.py b/hummingbot/strategy/fieldfare_market_making/__init__.py new file mode 100644 index 0000000000..fd33456004 --- /dev/null +++ b/hummingbot/strategy/fieldfare_market_making/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +from .fieldfare_market_making import FieldfareMarketMakingStrategy +__all__ = [ + FieldfareMarketMakingStrategy, +] diff --git a/hummingbot/strategy/fieldfare_mm/data_types.py b/hummingbot/strategy/fieldfare_market_making/data_types.py similarity index 100% rename from hummingbot/strategy/fieldfare_mm/data_types.py rename to hummingbot/strategy/fieldfare_market_making/data_types.py diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd similarity index 90% rename from hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd rename to hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd index 607c6a74dd..8811f5c691 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pxd +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd @@ -5,7 +5,7 @@ from hummingbot.strategy.strategy_base cimport StrategyBase from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator -cdef class FieldfareMMStrategy(StrategyBase): +cdef class FieldfareMarketMakingStrategy(StrategyBase): cdef: object _market_info object _minimum_spread @@ -29,9 +29,10 @@ cdef class FieldfareMMStrategy(StrategyBase): double _status_report_interval int64_t _logging_options object _last_own_trade_price - int _buffer_sampling_period + 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 @@ -41,12 +42,12 @@ cdef class FieldfareMMStrategy(StrategyBase): object _eta object _closing_time object _time_left - object _q_ajustment_factor + object _q_adjustment_factor object _reserved_price object _optimal_spread object _optimal_bid object _optimal_ask - double _latest_parameter_calculation_vol + object _latest_parameter_calculation_vol str _debug_csv_path object _avg_vol @@ -70,4 +71,3 @@ cdef class FieldfareMMStrategy(StrategyBase): cdef c_calculate_reserved_price_and_optimal_spread(self) cdef object c_calculate_target_inventory(self) cdef c_recalculate_parameters(self) - cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol) diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx similarity index 95% rename from hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx rename to hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx index ed3944b95c..a72adc6b94 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm.pyx +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx @@ -11,6 +11,7 @@ from math import ( ceil ) import time +import datetime import os from hummingbot.core.clock cimport Clock from hummingbot.core.event.events import TradeType @@ -40,7 +41,7 @@ s_decimal_one = Decimal(1) pmm_logger = None -cdef class FieldfareMMStrategy(StrategyBase): +cdef class FieldfareMarketMakingStrategy(StrategyBase): OPTION_LOG_CREATE_ORDER = 1 << 3 OPTION_LOG_MAKER_ORDER_FILLED = 1 << 4 OPTION_LOG_STATUS_REPORT = 1 << 5 @@ -79,8 +80,8 @@ cdef class FieldfareMMStrategy(StrategyBase): order_amount_shape_factor: Decimal = Decimal("0.005"), closing_time: Decimal = Decimal("1"), debug_csv_path: str = '', - buffer_size: int = 30, - buffer_sampling_period: int = 60 + volatility_buffer_size: int = 30, + volatility_sampling_period: int = 60 ): super().__init__() self._sb_order_tracker = OrderTracker() @@ -107,21 +108,21 @@ cdef class FieldfareMMStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) + self._ticks_to_be_ready = volatility_buffer_size * volatility_sampling_period 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(buffer_size, 1) - self._buffer_sampling_period = buffer_sampling_period + self._avg_vol = AverageVolatilityIndicator(volatility_buffer_size, 1) + self._volatility_sampling_period = volatility_sampling_period 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._q_ajustment_factor = Decimal("10")/self._order_amount - self._latest_parameter_calculation_vol = 0 + 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 @@ -311,7 +312,7 @@ cdef class FieldfareMMStrategy(StrategyBase): 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"Ref Price (MidPrice)"] + 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: @@ -357,7 +358,7 @@ cdef class FieldfareMMStrategy(StrategyBase): f" risk_factor(\u03B3)= {self._gamma:.5E}", f" order_book_depth_factor(\u03BA)= {self._kappa:.5E}", f" volatility= {volatility_pct:.3f}%", - f" time left fraction= {self._time_left/self._closing_time:.4f}"]) + 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])) @@ -412,7 +413,7 @@ cdef class FieldfareMMStrategy(StrategyBase): # so parameters need to be recalculated. if (self._gamma is None) or (self._kappa is None) or \ (self._parameters_based_on_spread and - self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value) > (self._vol_to_spread_multiplier - 1)): + 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() @@ -436,15 +437,20 @@ cdef class FieldfareMMStrategy(StrategyBase): if self.c_to_create_orders(proposal): self.c_execute_orders_proposal(proposal) else: - self.logger().info(f"Algorithm not ready...") + 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): - if timestamp - self._last_sampling_timestamp >= self._buffer_sampling_period: + if timestamp - self._last_sampling_timestamp >= self._volatility_sampling_period: self._avg_vol.add_sample(self.get_price()) self._last_sampling_timestamp = timestamp self._time_left = max(self._time_left - Decimal(timestamp - self._last_timestamp) * 1000, 0) + # Calculate adjustment factor to have 0.01% of inventory resolution + self._q_adjustment_factor = Decimal( + "1e5") / self.c_calculate_target_inventory() * self._inventory_target_base_pct if self._time_left == 0: # Re-cycle algorithm self._time_left = self._closing_time @@ -452,10 +458,10 @@ cdef class FieldfareMMStrategy(StrategyBase): self.c_recalculate_parameters() self.logger().info("Recycling algorithm time left and parameters if needed.") - cdef c_volatility_diff_from_last_parameter_calculation(self, double current_vol): + def volatility_diff_from_last_parameter_calculation(self, current_vol): if self._latest_parameter_calculation_vol == 0: - return 0 - return abs(self._latest_parameter_calculation_vol - current_vol) / self._latest_parameter_calculation_vol + 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: @@ -464,6 +470,16 @@ cdef class FieldfareMMStrategy(StrategyBase): 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 @@ -471,8 +487,8 @@ cdef class FieldfareMMStrategy(StrategyBase): 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_ajustment_factor - vol = Decimal(str(self._avg_vol.current_value)) + 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) @@ -499,7 +515,7 @@ cdef class FieldfareMMStrategy(StrategyBase): # Optimal bid and optimal ask prices will be used 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_ajustment_factor:.4f} | " + f"q={q/self._q_adjustment_factor:.4f} | " f"vol={vol:.4f}") cdef object c_calculate_target_inventory(self): @@ -525,11 +541,11 @@ cdef class FieldfareMMStrategy(StrategyBase): cdef: ExchangeBase market = self._market_info.market - q = (market.get_balance(self.base_asset) - self.c_calculate_target_inventory()) * self._q_ajustment_factor - vol = Decimal(str(self._avg_vol.current_value)) + 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 vol > 0 and q != 0: + if q != 0: min_spread = self._min_spread * price max_spread = self._max_spread * price @@ -988,7 +1004,7 @@ cdef class FieldfareMMStrategy(StrategyBase): self._gamma, self._kappa, self._eta, - self.c_volatility_diff_from_last_parameter_calculation(self._avg_vol.current_value), + self.volatility_diff_from_last_parameter_calculation(self.get_volatility()), self.inventory_target_base_pct, self._min_spread, self._max_spread, diff --git a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py similarity index 70% rename from hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py rename to hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py index a30691c486..cb811d4879 100644 --- a/hummingbot/strategy/fieldfare_mm/fieldfare_mm_config_map.py +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py @@ -22,7 +22,7 @@ def maker_trading_pair_prompt(): - exchange = fieldfare_mm_config_map.get("exchange").value + exchange = fieldfare_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 "") @@ -30,27 +30,27 @@ def maker_trading_pair_prompt(): # strategy specific validators def validate_exchange_trading_pair(value: str) -> Optional[str]: - exchange = fieldfare_mm_config_map.get("exchange").value + exchange = fieldfare_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 fieldfare_mm_config_map["min_spread"].value is not None: - min_spread = Decimal(fieldfare_mm_config_map["min_spread"].value) + if fieldfare_market_making_config_map["min_spread"].value is not None: + min_spread = Decimal(fieldfare_market_making_config_map["min_spread"].value) max_spread = Decimal(value) - if min_spread > max_spread: - return f"Max spread cannot be lesser than min spread {max_spread}%<{min_spread}%" + 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 - fieldfare_mm_config_map["max_spread"].value = None + fieldfare_market_making_config_map["max_spread"].value = None async def order_amount_prompt() -> str: - exchange = fieldfare_mm_config_map["exchange"].value - trading_pair = fieldfare_mm_config_map["market"].value + exchange = fieldfare_market_making_config_map["exchange"].value + trading_pair = fieldfare_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}) >>> " @@ -58,8 +58,8 @@ async def order_amount_prompt() -> str: async def validate_order_amount(value: str) -> Optional[str]: try: - exchange = fieldfare_mm_config_map["exchange"].value - trading_pair = fieldfare_mm_config_map["market"].value + exchange = fieldfare_market_making_config_map["exchange"].value + trading_pair = fieldfare_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}." @@ -69,7 +69,7 @@ async def validate_order_amount(value: str) -> Optional[str]: def on_validated_price_source_exchange(value: str): if value is None: - fieldfare_mm_config_map["price_source_market"].value = None + fieldfare_market_making_config_map["price_source_market"].value = None def exchange_on_validated(value: str): @@ -78,21 +78,21 @@ def exchange_on_validated(value: str): def on_validated_parameters_based_on_spread(value: str): if value == 'True': - fieldfare_mm_config_map.get("risk_factor").value = None - fieldfare_mm_config_map.get("order_book_depth_factor").value = None - fieldfare_mm_config_map.get("order_amount_shape_factor").value = None + fieldfare_market_making_config_map.get("risk_factor").value = None + fieldfare_market_making_config_map.get("order_book_depth_factor").value = None + fieldfare_market_making_config_map.get("order_amount_shape_factor").value = None else: - fieldfare_mm_config_map.get("max_spread").value = None - fieldfare_mm_config_map.get("min_spread").value = None - fieldfare_mm_config_map.get("vol_to_spread_multiplier").value = None - fieldfare_mm_config_map.get("inventory_risk_aversion").value = None + fieldfare_market_making_config_map.get("max_spread").value = None + fieldfare_market_making_config_map.get("min_spread").value = None + fieldfare_market_making_config_map.get("vol_to_spread_multiplier").value = None + fieldfare_market_making_config_map.get("inventory_risk_aversion").value = None -fieldfare_mm_config_map = { +fieldfare_market_making_config_map = { "strategy": ConfigVar(key="strategy", prompt=None, - default="fieldfare_mm"), + default="fieldfare_market_making"), "exchange": ConfigVar(key="exchange", prompt="Enter your maker exchange name >>> ", @@ -129,7 +129,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the minimum spread allowed from mid-price in percentage " "(Enter 1 to indicate 1%) >>> ", type_str="decimal", - required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_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), @@ -138,23 +138,24 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the maximum spread allowed from mid-price in percentage " "(Enter 1 to indicate 1%) >>> ", type_str="decimal", - required_if=lambda: fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_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-to-Spread multiplier: " - "Beyond this number of sigmas, spreads will turn into multiples of volatility >>>", + prompt="Enter the Volatility threshold multiplier: " + "(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: fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), prompt_on_new=True), "inventory_risk_aversion": ConfigVar(key="inventory_risk_aversion", - prompt="Enter Inventory risk aversion: With 1.0 being extremely conservative about meeting inventory target, " - "at the expense of profit, and 0.0 for a profit driven, at the expense of inventory risk >>>", + 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: fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: fieldfare_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": @@ -162,7 +163,7 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="order_book_depth_factor(\u03BA)", prompt="Enter order book depth factor (\u03BA) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_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": @@ -170,7 +171,7 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="risk_factor(\u03B3)", prompt="Enter risk factor (\u03B3) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_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": @@ -178,14 +179,13 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="order_amount_shape_factor(\u03B7)", prompt="Enter order amount shape factor (\u03B7) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_mm_config_map.get("parameters_based_on_spread").value, + required_if=lambda: not fieldfare_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 algorithm closing time in days. " - "When this time is reached, spread equations will recycle t=0" - " (fractional quantities are allowed i.e. 1.27 days) >>> ", + 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")), @@ -223,7 +223,7 @@ def on_validated_parameters_based_on_spread(value: str): default=60), "inventory_target_base_pct": ConfigVar(key="inventory_target_base_pct", - prompt="What is your target base asset percentage? Enter 50 for 50% >>> ", + 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, @@ -234,15 +234,16 @@ def on_validated_parameters_based_on_spread(value: str): type_str="bool", default=False, validator=validate_bool), - "buffer_size": - ConfigVar(key="buffer_size", - prompt="Enter amount of samples to use for volatility calculation>>> ", + "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), - "buffer_sampling_period": - ConfigVar(key="buffer_sampling_period", - prompt="Enter period in seconds of sampling for volatility calculation>>> ", + "volatility_sampling_period": + ConfigVar(key="volatility_sampling_period", + prompt="Enter how many seconds to wait between registering ticks for the volatility calculation. " + "(If set to 5, every 5 seconds a new sample will be stored)>>> ", type_str="int", validator=lambda v: validate_decimal(v, 1, 300), default=1), diff --git a/hummingbot/strategy/fieldfare_mm/start.py b/hummingbot/strategy/fieldfare_market_making/start.py similarity index 86% rename from hummingbot/strategy/fieldfare_mm/start.py rename to hummingbot/strategy/fieldfare_market_making/start.py index 95557d3521..f684973d95 100644 --- a/hummingbot/strategy/fieldfare_mm/start.py +++ b/hummingbot/strategy/fieldfare_market_making/start.py @@ -7,10 +7,10 @@ import os.path from hummingbot.client.hummingbot_application import HummingbotApplication from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple -from hummingbot.strategy.fieldfare_mm import ( - FieldfareMMStrategy, +from hummingbot.strategy.fieldfare_market_making import ( + FieldfareMarketMakingStrategy, ) -from hummingbot.strategy.fieldfare_mm.fieldfare_mm_config_map import fieldfare_mm_config_map as c_map +from hummingbot.strategy.fieldfare_market_making.fieldfare_market_making_config_map import fieldfare_market_making_config_map as c_map from decimal import Decimal import pandas as pd @@ -37,7 +37,7 @@ def start(self): maker_data = [self.markets[exchange], trading_pair] + list(maker_assets) self.market_trading_pair_tuples = [MarketTradingPairTuple(*maker_data)] - strategy_logging_options = FieldfareMMStrategy.OPTION_LOG_ALL + strategy_logging_options = FieldfareMarketMakingStrategy.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 @@ -51,13 +51,13 @@ def start(self): 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) - buffer_size = c_map.get("buffer_size").value - buffer_sampling_period = c_map.get("buffer_sampling_period").value + volatility_buffer_size = c_map.get("volatility_buffer_size").value + volatility_sampling_period = c_map.get("volatility_sampling_period").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 = FieldfareMMStrategy( + self.strategy = FieldfareMarketMakingStrategy( market_info=MarketTradingPairTuple(*maker_data), order_amount=order_amount, order_optimization_enabled=order_optimization_enabled, @@ -78,8 +78,8 @@ def start(self): order_amount_shape_factor=order_amount_shape_factor, closing_time=closing_time, debug_csv_path=debug_csv_path, - buffer_size=buffer_size, - buffer_sampling_period=buffer_sampling_period, + volatility_buffer_size=volatility_buffer_size, + volatility_sampling_period=volatility_sampling_period, ) except Exception as e: self._notify(str(e)) diff --git a/hummingbot/strategy/fieldfare_mm/__init__.py b/hummingbot/strategy/fieldfare_mm/__init__.py deleted file mode 100644 index bc05ca2af4..0000000000 --- a/hummingbot/strategy/fieldfare_mm/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -from .fieldfare_mm import FieldfareMMStrategy -__all__ = [ - FieldfareMMStrategy, -] diff --git a/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml b/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml similarity index 96% rename from hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml rename to hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml index 4c02c2e7ae..0ad224a613 100644 --- a/hummingbot/templates/conf_fieldfare_mm_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml @@ -49,5 +49,5 @@ order_amount_shape_factor: null closing_time: null # Buffer size used to store historic samples and calculate volatility -buffer_size: 60 -buffer_sampling_period: 1 +volatility_buffer_size: 60 +volatility_sampling_period: 1 From b4661b362e91dac0a7de5b1a0102b1ae5daafbb5 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Fri, 26 Mar 2021 18:04:51 -0300 Subject: [PATCH 41/77] Fixed bug in the strategy status message. Removed buffer_sampling_size --- .../fieldfare_market_making.pyx | 20 +++++++++---------- .../fieldfare_market_making_config_map.py | 7 ------- .../strategy/fieldfare_market_making/start.py | 2 -- ...ldfare_market_making_strategy_TEMPLATE.yml | 3 +-- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx index a72adc6b94..4b39eadcd2 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx @@ -81,7 +81,6 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): closing_time: Decimal = Decimal("1"), debug_csv_path: str = '', volatility_buffer_size: int = 30, - volatility_sampling_period: int = 60 ): super().__init__() self._sb_order_tracker = OrderTracker() @@ -108,14 +107,13 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): self._last_own_trade_price = Decimal('nan') self.c_add_markets([market_info.market]) - self._ticks_to_be_ready = volatility_buffer_size * volatility_sampling_period + 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._volatility_sampling_period = volatility_sampling_period self._last_sampling_timestamp = 0 self._kappa = order_book_depth_factor self._gamma = risk_factor @@ -354,11 +352,12 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): lines.extend(["", " No active maker orders."]) volatility_pct = self._avg_vol.current_value / float(self.get_price()) * 100.0 - 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))}"]) + 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])) @@ -444,9 +443,8 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): self._last_timestamp = timestamp cdef c_collect_market_variables(self, double timestamp): - if timestamp - self._last_sampling_timestamp >= self._volatility_sampling_period: - self._avg_vol.add_sample(self.get_price()) - self._last_sampling_timestamp = timestamp + self._avg_vol.add_sample(self.get_price()) + self._last_sampling_timestamp = timestamp self._time_left = max(self._time_left - Decimal(timestamp - self._last_timestamp) * 1000, 0) # Calculate adjustment factor to have 0.01% of inventory resolution self._q_adjustment_factor = Decimal( diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py index cb811d4879..00f89f6a6a 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py @@ -240,11 +240,4 @@ def on_validated_parameters_based_on_spread(value: str): type_str="int", validator=lambda v: validate_decimal(v, 5, 600), default=60), - "volatility_sampling_period": - ConfigVar(key="volatility_sampling_period", - prompt="Enter how many seconds to wait between registering ticks for the volatility calculation. " - "(If set to 5, every 5 seconds a new sample will be stored)>>> ", - type_str="int", - validator=lambda v: validate_decimal(v, 1, 300), - default=1), } diff --git a/hummingbot/strategy/fieldfare_market_making/start.py b/hummingbot/strategy/fieldfare_market_making/start.py index f684973d95..f1ea908fff 100644 --- a/hummingbot/strategy/fieldfare_market_making/start.py +++ b/hummingbot/strategy/fieldfare_market_making/start.py @@ -52,7 +52,6 @@ def start(self): 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 - volatility_sampling_period = c_map.get("volatility_sampling_period").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") @@ -79,7 +78,6 @@ def start(self): closing_time=closing_time, debug_csv_path=debug_csv_path, volatility_buffer_size=volatility_buffer_size, - volatility_sampling_period=volatility_sampling_period, ) except Exception as e: self._notify(str(e)) diff --git a/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml index 0ad224a613..2a05cf1c1b 100644 --- a/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml @@ -49,5 +49,4 @@ order_amount_shape_factor: null closing_time: null # Buffer size used to store historic samples and calculate volatility -volatility_buffer_size: 60 -volatility_sampling_period: 1 +volatility_buffer_size: 60 \ No newline at end of file From 3c6c36e10e1e0e285ae4a1751e3ef977881a34d1 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Fri, 26 Mar 2021 19:20:09 -0300 Subject: [PATCH 42/77] changed vol_to_spread_multiplier lower limit to 1 --- .../fieldfare_market_making_config_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py index 00f89f6a6a..6fe29cadd6 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py @@ -147,7 +147,7 @@ def on_validated_parameters_based_on_spread(value: str): "(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: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, - validator=lambda v: validate_decimal(v, 0, 10, inclusive=False), + validator=lambda v: validate_decimal(v, 1, 10, inclusive=False), prompt_on_new=True), "inventory_risk_aversion": ConfigVar(key="inventory_risk_aversion", From 5b1c770cc7381d988e6b2a44bda4f648971fef2a Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Sun, 28 Mar 2021 13:39:06 -0300 Subject: [PATCH 43/77] Changed the inventory base over which to calculate q_where_to_decay. It should be the desired target in q, not the total inventory in q --- .../fieldfare_market_making/fieldfare_market_making.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx index 4b39eadcd2..23797d8a2e 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx @@ -564,8 +564,7 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): self._kappa = self._gamma / (Decimal.exp((max_spread_around_reserved_price * self._gamma - (vol * self._gamma) **2) / 2) - 1) # ETA - total_inventory_in_base = self.c_calculate_target_inventory() / self._inventory_target_base_pct - q_where_to_decay_order_amount = total_inventory_in_base * (1 - self._inventory_risk_aversion) + q_where_to_decay_order_amount = self.c_calculate_target_inventory() * (1 - self._inventory_risk_aversion) self._eta = s_decimal_one / q_where_to_decay_order_amount self._latest_parameter_calculation_vol = vol From 1af339ec8155dbfcfe1e89d5c957889bb785cd78 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:45:00 +0800 Subject: [PATCH 44/77] (feat) add oracle to XEMM --- hummingbot/core/rate_oracle/utils.py | 2 ++ .../arbitrage/arbitrage_config_map.py | 1 - .../cross_exchange_market_making.pxd | 1 + .../cross_exchange_market_making.pyx | 13 ++++++- ...cross_exchange_market_making_config_map.py | 34 ++++++++++++++++--- .../cross_exchange_market_making/start.py | 2 ++ ...change_market_making_strategy_TEMPLATE.yml | 6 +++- 7 files changed, 51 insertions(+), 8 deletions(-) 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/arbitrage/arbitrage_config_map.py b/hummingbot/strategy/arbitrage/arbitrage_config_map.py index c903f8a022..c518e9f15e 100644 --- a/hummingbot/strategy/arbitrage/arbitrage_config_map.py +++ b/hummingbot/strategy/arbitrage/arbitrage_config_map.py @@ -40,7 +40,6 @@ def secondary_market_on_validated(value: str): def use_oracle_conversion_rate_on_validated(value: str): - # global required_rate_oracle, rate_oracle_pairs use_oracle = parse_cvar_value(arbitrage_config_map["use_oracle_conversion_rate"], value) first_base, first_quote = arbitrage_config_map["primary_market_trading_pair"].value.split("-") second_base, second_quote = arbitrage_config_map["secondary_market_trading_pair"].value.split("-") 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..dd03835aa2 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,6 +31,7 @@ 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 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..cd04cbcdbd 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 @@ -28,6 +28,7 @@ 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 NaN = float("nan") s_decimal_zero = Decimal(0) @@ -73,6 +74,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,6 +134,7 @@ 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._hb_app_notification = hb_app_notification @@ -1096,7 +1099,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 + if not self._use_oracle_conversion_rate: + return self._taker_to_maker_quote_conversion_rate / self._taker_to_maker_base_conversion_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): """ 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..c34273173c 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,23 @@ 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 use_oracle_conversion_rate_on_validated(value: str): + use_oracle = parse_cvar_value(cross_exchange_market_making_config_map["use_oracle_conversion_rate"], value) + first_base, first_quote = cross_exchange_market_making_config_map["maker_market_trading_pair"].value.split("-") + second_base, second_quote = cross_exchange_market_making_config_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 +98,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", @@ -193,6 +210,13 @@ 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=use_oracle_conversion_rate_on_validated), "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. " 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_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) From c435955af12e0b25334c6e12636b1055a7aff72f Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Mon, 29 Mar 2021 12:44:18 +0800 Subject: [PATCH 45/77] (feat) add USD price to Binance and add doc strings --- hummingbot/client/config/global_config_map.py | 4 +- hummingbot/core/rate_oracle/rate_oracle.py | 111 +++++++++++++++--- test/test_rate_oracle.py | 17 +++ 3 files changed, 115 insertions(+), 17 deletions(-) diff --git a/hummingbot/client/config/global_config_map.py b/hummingbot/client/config/global_config_map.py index 31bd7e566d..0ab05485fe 100644 --- a/hummingbot/client/config/global_config_map.py +++ b/hummingbot/client/config/global_config_map.py @@ -361,11 +361,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/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/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)) From 18d496ac5bfc8d02314412cd2d6d71552ddb6658 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 29 Mar 2021 02:24:47 -0300 Subject: [PATCH 46/77] Added is_debug parameter to turn on and off debugging. For release commit it will be set to False --- .../fieldfare_market_making.pxd | 2 +- .../fieldfare_market_making.pyx | 17 +++++++++++------ .../strategy/fieldfare_market_making/start.py | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd index 8811f5c691..bd7c4ec57e 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd @@ -2,7 +2,6 @@ from libc.stdint cimport int64_t from hummingbot.strategy.strategy_base cimport StrategyBase -from ..__utils__.trailing_indicators.average_volatility import AverageVolatilityIndicator cdef class FieldfareMarketMakingStrategy(StrategyBase): @@ -18,6 +17,7 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): bint _order_optimization_enabled bint _add_transaction_costs_to_orders bint _hb_app_notification + bint _is_debug double _cancel_timestamp double _create_timestamp diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx index 23797d8a2e..10e817f769 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx @@ -81,6 +81,7 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): 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() @@ -126,8 +127,10 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): 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: - os.unlink(self._debug_csv_path) + if self._is_debug: + os.unlink(self._debug_csv_path) except FileNotFoundError: pass @@ -428,7 +431,8 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): self.c_apply_budget_constraint(proposal) self.c_cancel_active_orders(proposal) - self.dump_debug_variables() + 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: @@ -511,10 +515,11 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): 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 - 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}") + 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: diff --git a/hummingbot/strategy/fieldfare_market_making/start.py b/hummingbot/strategy/fieldfare_market_making/start.py index f1ea908fff..2dd85a7318 100644 --- a/hummingbot/strategy/fieldfare_market_making/start.py +++ b/hummingbot/strategy/fieldfare_market_making/start.py @@ -78,6 +78,7 @@ def start(self): closing_time=closing_time, debug_csv_path=debug_csv_path, volatility_buffer_size=volatility_buffer_size, + is_debug=True ) except Exception as e: self._notify(str(e)) From 550e1c0fd77a2bec2b07b385cda5af724d5467c0 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Mon, 29 Mar 2021 02:54:16 -0300 Subject: [PATCH 47/77] Fixed bug where inventory_target=0 would make calculations crash --- .../fieldfare_market_making.pyx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx index 10e817f769..246210d8ca 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx @@ -447,12 +447,17 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): self._last_timestamp = timestamp cdef c_collect_market_variables(self, double timestamp): - self._avg_vol.add_sample(self.get_price()) + 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") / self.c_calculate_target_inventory() * self._inventory_target_base_pct + "1e5") / inventory_in_base if self._time_left == 0: # Re-cycle algorithm self._time_left = self._closing_time @@ -569,8 +574,11 @@ cdef class FieldfareMarketMakingStrategy(StrategyBase): 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 / q_where_to_decay_order_amount + 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 From e7e0ff41375e8ef03717d97cf14619ea6f18a3eb Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 31 Mar 2021 01:07:06 -0300 Subject: [PATCH 48/77] Changed connector prompt message to make only spot connectors appear in the autocomplete --- .../fieldfare_market_making_config_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py index 6fe29cadd6..ac4187474a 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py @@ -95,7 +95,7 @@ def on_validated_parameters_based_on_spread(value: str): default="fieldfare_market_making"), "exchange": ConfigVar(key="exchange", - prompt="Enter your maker exchange name >>> ", + prompt="Enter your maker spot connector >>> ", validator=validate_exchange, on_validated=exchange_on_validated, prompt_on_new=True), From 842bc60b551144b737b48577a93fa54d515a6976 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Wed, 31 Mar 2021 17:14:38 +0800 Subject: [PATCH 49/77] (feat) add rate conversion table on status and log the rates --- hummingbot/strategy/arbitrage/arbitrage.pxd | 1 + hummingbot/strategy/arbitrage/arbitrage.pyx | 82 +++++++++++++++++-- .../cross_exchange_market_making.pxd | 1 + .../cross_exchange_market_making.pyx | 75 +++++++++++++++-- hummingbot/templates/conf_global_TEMPLATE.yml | 3 + 5 files changed, 149 insertions(+), 13 deletions(-) diff --git a/hummingbot/strategy/arbitrage/arbitrage.pxd b/hummingbot/strategy/arbitrage/arbitrage.pxd index 56eed3b065..9d999eea9d 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pxd +++ b/hummingbot/strategy/arbitrage/arbitrage.pxd @@ -28,6 +28,7 @@ cdef class ArbitrageStrategy(StrategyBase): 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 7090a2279f..b406d9a742 100755 --- a/hummingbot/strategy/arbitrage/arbitrage.pyx +++ b/hummingbot/strategy/arbitrage/arbitrage.pyx @@ -21,6 +21,7 @@ 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) @@ -80,6 +81,7 @@ cdef class ArbitrageStrategy(StrategyBase): 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 @@ -108,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 = [] @@ -119,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")]) @@ -196,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 @@ -392,14 +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: - if not self._use_oracle_conversion_rate: - return self._secondary_to_primary_quote_conversion_rate / self._secondary_to_primary_base_conversion_rate - else: - quote_pair = f"{self._market_pairs[0].second.quote_asset}-{self._market_pairs[0].first.quote_asset}" - base_pair = f"{self._market_pairs[0].second.base_asset}-{self._market_pairs[0].first.base_asset}" - quote_rate = RateOracle.get_instance().rate(quote_pair) - base_rate = RateOracle.get_instance().rate(base_pair) - return quote_rate / base_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/cross_exchange_market_making/cross_exchange_market_making.pxd b/hummingbot/strategy/cross_exchange_market_making/cross_exchange_market_making.pxd index dd03835aa2..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 @@ -36,6 +36,7 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): 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 cd04cbcdbd..c265968ff1 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, @@ -29,6 +30,7 @@ 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) @@ -137,6 +139,7 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): 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 = [] @@ -170,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 = [] @@ -193,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")]) @@ -308,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 @@ -1102,12 +1164,15 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): if not self._use_oracle_conversion_rate: return self._taker_to_maker_quote_conversion_rate / self._taker_to_maker_base_conversion_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) + _, _, 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): """ diff --git a/hummingbot/templates/conf_global_TEMPLATE.yml b/hummingbot/templates/conf_global_TEMPLATE.yml index 2d3af846f3..0278a5c05a 100644 --- a/hummingbot/templates/conf_global_TEMPLATE.yml +++ b/hummingbot/templates/conf_global_TEMPLATE.yml @@ -190,8 +190,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 From ed4df6439b5f8c9064cb56ea2bf656a5a90ecf17 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 31 Mar 2021 22:28:57 -0300 Subject: [PATCH 50/77] Added accurate limit for vol_to_spread_multiplier in the prompt --- .../fieldfare_market_making_config_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py index ac4187474a..8a69befc56 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py +++ b/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py @@ -143,7 +143,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt_on_new=True), "vol_to_spread_multiplier": ConfigVar(key="vol_to_spread_multiplier", - prompt="Enter the Volatility threshold 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: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, From f7460c16d9745719b4ba4990ec2ca5725d1ea507 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Wed, 31 Mar 2021 23:35:59 -0300 Subject: [PATCH 51/77] Turned off debug messages for fieldfare strategy --- hummingbot/strategy/fieldfare_market_making/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hummingbot/strategy/fieldfare_market_making/start.py b/hummingbot/strategy/fieldfare_market_making/start.py index 2dd85a7318..554979dcc2 100644 --- a/hummingbot/strategy/fieldfare_market_making/start.py +++ b/hummingbot/strategy/fieldfare_market_making/start.py @@ -78,7 +78,7 @@ def start(self): closing_time=closing_time, debug_csv_path=debug_csv_path, volatility_buffer_size=volatility_buffer_size, - is_debug=True + is_debug=False ) except Exception as e: self._notify(str(e)) From da6f05007b83075c2832888af8da79ac192e6885 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Thu, 1 Apr 2021 11:19:13 +0800 Subject: [PATCH 52/77] (fix) assign market initial value --- hummingbot/client/ui/completer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From a87eaa01f557b8cd4b0685ee26d2a83a7a248895 Mon Sep 17 00:00:00 2001 From: Nullably <37262506+Nullably@users.noreply.github.com> Date: Thu, 1 Apr 2021 16:15:08 +0800 Subject: [PATCH 53/77] (feat) update oracle settings on trading pair configs --- hummingbot/client/command/create_command.py | 2 + .../arbitrage/arbitrage_config_map.py | 60 ++++++++++++------- ...cross_exchange_market_making_config_map.py | 33 ++++++---- 3 files changed, 60 insertions(+), 35 deletions(-) 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/strategy/arbitrage/arbitrage_config_map.py b/hummingbot/strategy/arbitrage/arbitrage_config_map.py index c518e9f15e..781396f642 100644 --- a/hummingbot/strategy/arbitrage/arbitrage_config_map.py +++ b/hummingbot/strategy/arbitrage/arbitrage_config_map.py @@ -39,10 +39,15 @@ def secondary_market_on_validated(value: str): settings.required_exchanges.append(value) -def use_oracle_conversion_rate_on_validated(value: str): - use_oracle = parse_cvar_value(arbitrage_config_map["use_oracle_conversion_rate"], value) - first_base, first_quote = arbitrage_config_map["primary_market_trading_pair"].value.split("-") - second_base, second_quote = arbitrage_config_map["secondary_market_trading_pair"].value.split("-") +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 = [] @@ -56,60 +61,71 @@ def use_oracle_conversion_rate_on_validated(value: str): 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: settings.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=use_oracle_conversion_rate_on_validated), + 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/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 c34273173c..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 @@ -72,10 +72,15 @@ def taker_market_on_validated(value: str): settings.required_exchanges.append(value) -def use_oracle_conversion_rate_on_validated(value: str): - use_oracle = parse_cvar_value(cross_exchange_market_making_config_map["use_oracle_conversion_rate"], value) - first_base, first_quote = cross_exchange_market_making_config_map["maker_market_trading_pair"].value.split("-") - second_base, second_quote = cross_exchange_market_making_config_map["taker_market_trading_pair"].value.split("-") +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 = [] @@ -111,13 +116,15 @@ def use_oracle_conversion_rate_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", @@ -216,23 +223,23 @@ def use_oracle_conversion_rate_on_validated(value: str): 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=use_oracle_conversion_rate_on_validated), + 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" ), } From f68d76b00c25c32d8412166b918de472e22a62d8 Mon Sep 17 00:00:00 2001 From: Nicolas Baum Date: Thu, 1 Apr 2021 09:40:34 -0300 Subject: [PATCH 54/77] Renamed strategy to avellaneda_market_making_ --- .../avellaneda_market_making/__init__.py | 6 +++ .../avellaneda_market_making.pxd} | 2 +- .../avellaneda_market_making.pyx} | 2 +- .../avellaneda_market_making_config_map.py} | 52 +++++++++---------- .../data_types.py | 0 .../start.py | 10 ++-- .../fieldfare_market_making/__init__.py | 6 --- ...aneda_market_making_strategy_TEMPLATE.yml} | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 hummingbot/strategy/avellaneda_market_making/__init__.py rename hummingbot/strategy/{fieldfare_market_making/fieldfare_market_making.pxd => avellaneda_market_making/avellaneda_market_making.pxd} (97%) rename hummingbot/strategy/{fieldfare_market_making/fieldfare_market_making.pyx => avellaneda_market_making/avellaneda_market_making.pyx} (99%) rename hummingbot/strategy/{fieldfare_market_making/fieldfare_market_making_config_map.py => avellaneda_market_making/avellaneda_market_making_config_map.py} (81%) rename hummingbot/strategy/{fieldfare_market_making => avellaneda_market_making}/data_types.py (100%) rename hummingbot/strategy/{fieldfare_market_making => avellaneda_market_making}/start.py (91%) delete mode 100644 hummingbot/strategy/fieldfare_market_making/__init__.py rename hummingbot/templates/{conf_fieldfare_market_making_strategy_TEMPLATE.yml => conf_avellaneda_market_making_strategy_TEMPLATE.yml} (96%) 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/fieldfare_market_making/fieldfare_market_making.pxd b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd similarity index 97% rename from hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd rename to hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd index bd7c4ec57e..79df1f715f 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pxd +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pxd @@ -4,7 +4,7 @@ from libc.stdint cimport int64_t from hummingbot.strategy.strategy_base cimport StrategyBase -cdef class FieldfareMarketMakingStrategy(StrategyBase): +cdef class AvellanedaMarketMakingStrategy(StrategyBase): cdef: object _market_info object _minimum_spread diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx similarity index 99% rename from hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx rename to hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx index 246210d8ca..9704ec2032 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making.pyx +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making.pyx @@ -41,7 +41,7 @@ s_decimal_one = Decimal(1) pmm_logger = None -cdef class FieldfareMarketMakingStrategy(StrategyBase): +cdef class AvellanedaMarketMakingStrategy(StrategyBase): OPTION_LOG_CREATE_ORDER = 1 << 3 OPTION_LOG_MAKER_ORDER_FILLED = 1 << 4 OPTION_LOG_STATUS_REPORT = 1 << 5 diff --git a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py similarity index 81% rename from hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py rename to hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py index 8a69befc56..34a6617163 100644 --- a/hummingbot/strategy/fieldfare_market_making/fieldfare_market_making_config_map.py +++ b/hummingbot/strategy/avellaneda_market_making/avellaneda_market_making_config_map.py @@ -22,7 +22,7 @@ def maker_trading_pair_prompt(): - exchange = fieldfare_market_making_config_map.get("exchange").value + 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 "") @@ -30,14 +30,14 @@ def maker_trading_pair_prompt(): # strategy specific validators def validate_exchange_trading_pair(value: str) -> Optional[str]: - exchange = fieldfare_market_making_config_map.get("exchange").value + 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 fieldfare_market_making_config_map["min_spread"].value is not None: - min_spread = Decimal(fieldfare_market_making_config_map["min_spread"].value) + 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}%" @@ -45,12 +45,12 @@ def validate_max_spread(value: str) -> Optional[str]: def onvalidated_min_spread(value: str): # If entered valid min_spread, max_spread is invalidated so user sets it up again - fieldfare_market_making_config_map["max_spread"].value = None + avellaneda_market_making_config_map["max_spread"].value = None async def order_amount_prompt() -> str: - exchange = fieldfare_market_making_config_map["exchange"].value - trading_pair = fieldfare_market_making_config_map["market"].value + 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}) >>> " @@ -58,8 +58,8 @@ async def order_amount_prompt() -> str: async def validate_order_amount(value: str) -> Optional[str]: try: - exchange = fieldfare_market_making_config_map["exchange"].value - trading_pair = fieldfare_market_making_config_map["market"].value + 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}." @@ -69,7 +69,7 @@ async def validate_order_amount(value: str) -> Optional[str]: def on_validated_price_source_exchange(value: str): if value is None: - fieldfare_market_making_config_map["price_source_market"].value = None + avellaneda_market_making_config_map["price_source_market"].value = None def exchange_on_validated(value: str): @@ -78,21 +78,21 @@ def exchange_on_validated(value: str): def on_validated_parameters_based_on_spread(value: str): if value == 'True': - fieldfare_market_making_config_map.get("risk_factor").value = None - fieldfare_market_making_config_map.get("order_book_depth_factor").value = None - fieldfare_market_making_config_map.get("order_amount_shape_factor").value = None + 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: - fieldfare_market_making_config_map.get("max_spread").value = None - fieldfare_market_making_config_map.get("min_spread").value = None - fieldfare_market_making_config_map.get("vol_to_spread_multiplier").value = None - fieldfare_market_making_config_map.get("inventory_risk_aversion").value = None + 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 -fieldfare_market_making_config_map = { +avellaneda_market_making_config_map = { "strategy": ConfigVar(key="strategy", prompt=None, - default="fieldfare_market_making"), + default="avellaneda_market_making"), "exchange": ConfigVar(key="exchange", prompt="Enter your maker spot connector >>> ", @@ -129,7 +129,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the minimum spread allowed from mid-price in percentage " "(Enter 1 to indicate 1%) >>> ", type_str="decimal", - required_if=lambda: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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), @@ -138,7 +138,7 @@ def on_validated_parameters_based_on_spread(value: str): prompt="Enter the maximum spread allowed from mid-price in percentage " "(Enter 1 to indicate 1%) >>> ", type_str="decimal", - required_if=lambda: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": @@ -146,7 +146,7 @@ def on_validated_parameters_based_on_spread(value: str): 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: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": @@ -155,7 +155,7 @@ def on_validated_parameters_based_on_spread(value: str): "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: fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": @@ -163,7 +163,7 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="order_book_depth_factor(\u03BA)", prompt="Enter order book depth factor (\u03BA) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": @@ -171,7 +171,7 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="risk_factor(\u03B3)", prompt="Enter risk factor (\u03B3) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": @@ -179,7 +179,7 @@ def on_validated_parameters_based_on_spread(value: str): printable_key="order_amount_shape_factor(\u03B7)", prompt="Enter order amount shape factor (\u03B7) >>> ", type_str="decimal", - required_if=lambda: not fieldfare_market_making_config_map.get("parameters_based_on_spread").value, + 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": diff --git a/hummingbot/strategy/fieldfare_market_making/data_types.py b/hummingbot/strategy/avellaneda_market_making/data_types.py similarity index 100% rename from hummingbot/strategy/fieldfare_market_making/data_types.py rename to hummingbot/strategy/avellaneda_market_making/data_types.py diff --git a/hummingbot/strategy/fieldfare_market_making/start.py b/hummingbot/strategy/avellaneda_market_making/start.py similarity index 91% rename from hummingbot/strategy/fieldfare_market_making/start.py rename to hummingbot/strategy/avellaneda_market_making/start.py index 554979dcc2..2d12fe8a39 100644 --- a/hummingbot/strategy/fieldfare_market_making/start.py +++ b/hummingbot/strategy/avellaneda_market_making/start.py @@ -7,10 +7,10 @@ import os.path from hummingbot.client.hummingbot_application import HummingbotApplication from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple -from hummingbot.strategy.fieldfare_market_making import ( - FieldfareMarketMakingStrategy, +from hummingbot.strategy.avellaneda_market_making import ( + AvellanedaMarketMakingStrategy, ) -from hummingbot.strategy.fieldfare_market_making.fieldfare_market_making_config_map import fieldfare_market_making_config_map as c_map +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 @@ -37,7 +37,7 @@ def start(self): maker_data = [self.markets[exchange], trading_pair] + list(maker_assets) self.market_trading_pair_tuples = [MarketTradingPairTuple(*maker_data)] - strategy_logging_options = FieldfareMarketMakingStrategy.OPTION_LOG_ALL + 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 @@ -56,7 +56,7 @@ def start(self): HummingbotApplication.main_application().strategy_file_name.rsplit('.', 1)[0] + f"_{pd.Timestamp.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv") - self.strategy = FieldfareMarketMakingStrategy( + self.strategy = AvellanedaMarketMakingStrategy( market_info=MarketTradingPairTuple(*maker_data), order_amount=order_amount, order_optimization_enabled=order_optimization_enabled, diff --git a/hummingbot/strategy/fieldfare_market_making/__init__.py b/hummingbot/strategy/fieldfare_market_making/__init__.py deleted file mode 100644 index fd33456004..0000000000 --- a/hummingbot/strategy/fieldfare_market_making/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -from .fieldfare_market_making import FieldfareMarketMakingStrategy -__all__ = [ - FieldfareMarketMakingStrategy, -] diff --git a/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml similarity index 96% rename from hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml rename to hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml index 2a05cf1c1b..c4205052b4 100644 --- a/hummingbot/templates/conf_fieldfare_market_making_strategy_TEMPLATE.yml +++ b/hummingbot/templates/conf_avellaneda_market_making_strategy_TEMPLATE.yml @@ -1,5 +1,5 @@ ######################################################## -### Fieldfare market making strategy config ### +### Avellaneda market making strategy config ### ######################################################## template_version: 1 From e742a34aae90af121f83e21637163518d568f56f Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Thu, 25 Mar 2021 22:59:02 +0000 Subject: [PATCH 55/77] Fix / OrderBookTracker debug logging outputs --- hummingbot/core/data_type/order_book_tracker.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 From 4606e0e149c1d329f4c2737b99dcda4e629eff71 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:28:12 +0000 Subject: [PATCH 56/77] Fix / Bamboo Relay, Loopring, Radar Relay debug logging outputs --- .../bamboo_relay/bamboo_relay_order_book_tracker.py | 8 +++----- .../exchange/loopring/loopring_order_book_tracker.py | 2 +- .../radar_relay/radar_relay_order_book_tracker.py | 11 ++++------- 3 files changed, 8 insertions(+), 13 deletions(-) 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/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/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: From 68945c9be220d9bf20e0b5312c08b0217e53f5fc Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:27:49 +0000 Subject: [PATCH 57/77] Fix / Binance Perpetual debug logging outputs --- .../binance_perpetual_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From 382c986a00464f2e9daff8638393c21ee3267ff6 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:28:25 +0000 Subject: [PATCH 58/77] Fix / Beaxy debug logging outputs --- .../exchange/beaxy/beaxy_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From bdfb5a6806a0bef0efda6910569a55ee9f07046b Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:28:38 +0000 Subject: [PATCH 59/77] Fix / Binance debug logging outputs --- .../connector/exchange/binance/binance_exchange.pyx | 6 +++--- .../exchange/binance/binance_order_book_tracker.py | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) 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: From ae6b47ed9dbdf5cf110191cc928df99c33fb2919 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:28:49 +0000 Subject: [PATCH 60/77] Fix / Bitfinex debug logging outputs --- .../exchange/bitfinex/bitfinex_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From dfdbbc38cba4e4982238bd08f56b873d464d95d3 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:28:56 +0000 Subject: [PATCH 61/77] Fix / AscendEx debug logging outputs --- .../exchange/ascend_ex/ascend_ex_order_book_tracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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: From ad9efe7f456b92a634629b6f49158e704da3c73f Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:29:04 +0000 Subject: [PATCH 62/77] Fix / Bittrex debug logging outputs --- .../connector/exchange/bittrex/bittrex_order_book_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 112c1d7c8ea6221fb321e43520d207e91843a9fb Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:29:16 +0000 Subject: [PATCH 63/77] Fix / Blocktane debug logging outputs --- .../blocktane/blocktane_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From bd68acdebb08c60a1885d9abcc1ba5e78d1370c5 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:29:25 +0000 Subject: [PATCH 64/77] Fix / Coinbase Pro debug logging outputs --- .../coinbase_pro/coinbase_pro_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From a254879f74fbcad27d3ca7c5ff4ecc34a9542f8c Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:30:33 +0000 Subject: [PATCH 65/77] Fix / Crypto.com debug logging outputs --- .../exchange/crypto_com/crypto_com_order_book_tracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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: From b78f84cdc71a9a34faf507cca0ae7b4db4b2573a Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:30:40 +0000 Subject: [PATCH 66/77] Fix / Dolomite debug logging outputs --- .../connector/exchange/dolomite/dolomite_order_book_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c97ff8b34a3c10f1afb4222a0af291ce5405560a Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:30:47 +0000 Subject: [PATCH 67/77] Fix / Dydx debug logging outputs --- hummingbot/connector/exchange/dydx/dydx_order_book_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 24ca92afe4668edb434e1b45e30b3096317b97cb Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:30:56 +0000 Subject: [PATCH 68/77] Fix / Huobi debug logging outputs --- .../exchange/huobi/huobi_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From 1578e004022306fa5580221525ad4e3c6dc4eb2f Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:31:03 +0000 Subject: [PATCH 69/77] Fix / Kraken debug logging outputs --- .../connector/exchange/kraken/kraken_order_book_tracker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 From 7e11f296a4419828ecc0de2262854dd5f9328db7 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:31:10 +0000 Subject: [PATCH 70/77] Fix / Kucoin debug logging outputs --- .../exchange/kucoin/kucoin_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From a98518a550caa2b14db37034289ad0bc4cdaed12 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:31:18 +0000 Subject: [PATCH 71/77] Fix / Liquid debug logging outputs --- .../exchange/liquid/liquid_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From 218b5717f3b712c56131f3105cd866daa31140fb Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:31:24 +0000 Subject: [PATCH 72/77] Fix / Okex debug logging outputs --- .../exchange/okex/okex_order_book_tracker.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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: From d550968fb042293fd36b164854b4b249d329725d Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Fri, 26 Mar 2021 02:31:30 +0000 Subject: [PATCH 73/77] Fix / Probit debug logging outputs --- .../connector/exchange/probit/probit_order_book_tracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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: From 6ffac342fec216b105910be44765b79486d1503f Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Thu, 1 Apr 2021 16:50:52 +0100 Subject: [PATCH 74/77] XEMM: Fix Conversion Rate calculation issues --- .../cross_exchange_market_making.pyx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) 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 c265968ff1..88de032f42 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 @@ -1135,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) @@ -1202,15 +1205,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: @@ -1241,15 +1244,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: From 32021682963408a490f99c2861bdcbb95f850cb9 Mon Sep 17 00:00:00 2001 From: TheHolyRoger Date: Thu, 1 Apr 2021 17:11:52 +0100 Subject: [PATCH 75/77] XEMM: Remove redundant conversion rate check --- .../cross_exchange_market_making.pyx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 88de032f42..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 @@ -1164,11 +1164,8 @@ cdef class CrossExchangeMarketMakingStrategy(StrategyBase): """ Return price conversion rate for a taker market (to convert it into maker base asset value) """ - if not self._use_oracle_conversion_rate: - return self._taker_to_maker_quote_conversion_rate / self._taker_to_maker_base_conversion_rate - else: - _, _, quote_rate, _, _, base_rate = self.get_taker_to_maker_conversion_rate() - return quote_rate / base_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}" From 729503692f8172bfd9c535952fc5c25973cda159 Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Fri, 2 Apr 2021 13:24:03 +0800 Subject: [PATCH 76/77] (fix) fix AttributeError MSVCCompiler when building Windows binary --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8a0c12f2d2..52c241a0bb 100755 --- a/setup.py +++ b/setup.py @@ -176,7 +176,7 @@ def main(): "bin/hummingbot.py", "bin/hummingbot_quickstart.py" ], - cmdclass={'build_ext': BuildExt}, + cmdclass={'build_ext': BuildExt} if os.name != "nt" else {}, ) From 3e371c2bfb4854588351ac69af38128cbb9ad28d Mon Sep 17 00:00:00 2001 From: Daniel Tan Date: Fri, 2 Apr 2021 13:49:36 +0800 Subject: [PATCH 77/77] (cleanup) cleanup fix as per Patrick feedback --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 52c241a0bb..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() @@ -176,7 +176,7 @@ def main(): "bin/hummingbot.py", "bin/hummingbot_quickstart.py" ], - cmdclass={'build_ext': BuildExt} if os.name != "nt" else {}, + cmdclass={'build_ext': BuildExt}, )