diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py index 444609ce50..bf561679db 100644 --- a/hummingbot/client/command/config_command.py +++ b/hummingbot/client/command/config_command.py @@ -21,6 +21,9 @@ from hummingbot.client.config.config_var import ConfigVar from hummingbot.core.utils.async_utils import safe_ensure_future from hummingbot.model.inventory_cost import InventoryCost +from hummingbot.strategy.the_money_pit import ( + TheMoneyPitStrategy +) from hummingbot.strategy.pure_market_making import ( PureMarketMakingStrategy ) @@ -34,7 +37,29 @@ no_restart_pmm_keys_in_percentage = ["bid_spread", "ask_spread", "order_level_spread", "inventory_target_base_pct"] +pmm_k_append_perc = [ + "trade_gain_allowed_loss", + "trade_gain_profit_wanted", + "trade_gain_ownside_allowedloss", + "trade_gain_profit_selloff", + "trade_gain_profit_buyin", + "market_indicator_reduce_orders_to_pct", +] +no_restart_pmm_keys_in_percentage = no_restart_pmm_keys_in_percentage + pmm_k_append_perc no_restart_pmm_keys = ["order_amount", "order_levels", "filled_order_delay", "inventory_skew_enabled", "inventory_range_multiplier"] +pmm_k_append = [ + "trade_gain_enabled", + "trade_gain_hours", + "trade_gain_trades", + "trade_gain_ownside_enabled", + "trade_gain_careful_enabled", + "trade_gain_careful_limittrades", + "trade_gain_careful_hours", + "trade_gain_initial_max_buy", + "trade_gain_initial_min_sell", + "market_indicator_allow_profitable", +] +no_restart_pmm_keys = no_restart_pmm_keys + pmm_k_append global_configs_to_display = ["0x_active_cancels", "kill_switch_enabled", "kill_switch_rate", @@ -161,6 +186,7 @@ async def _config_single_key(self, # type: HummingbotApplication for config in missings: self._notify(f"{config.key}: {str(config.value)}") if isinstance(self.strategy, PureMarketMakingStrategy) or \ + isinstance(self.strategy, TheMoneyPitStrategy) or \ isinstance(self.strategy, PerpetualMarketMakingStrategy): updated = ConfigCommand.update_running_mm(self.strategy, key, config_var.value) if updated: diff --git a/hummingbot/data_feed/market_indicator_data_feed.py b/hummingbot/data_feed/market_indicator_data_feed.py new file mode 100644 index 0000000000..d7b6ff0a74 --- /dev/null +++ b/hummingbot/data_feed/market_indicator_data_feed.py @@ -0,0 +1,138 @@ +import asyncio +import aiohttp +import logging +import time +from typing import Optional +from hummingbot.core.network_base import NetworkBase, NetworkStatus +from hummingbot.logger import HummingbotLogger +from hummingbot.core.utils.async_utils import safe_ensure_future +from decimal import Decimal +from urllib.parse import urlparse + + +class MarketIndicatorDataFeed(NetworkBase): + cadf_logger: Optional[HummingbotLogger] = None + + @classmethod + def logger(cls) -> HummingbotLogger: + if cls.cadf_logger is None: + cls.cadf_logger = logging.getLogger(__name__) + return cls.cadf_logger + + def __init__(self, + api_url, + api_key: str = "", + update_interval: float = 30.0, + check_expiry: bool = False, + expire_time: int = 300, + use_indicator_time: bool = False): + super().__init__() + self._ready_event = asyncio.Event() + self._shared_client: Optional[aiohttp.ClientSession] = None + self._api_url = api_url + self._api_name = urlparse(api_url).netloc + self._api_auth_params = {'api_key': api_key} + self._check_network_interval = 120.0 + self._ev_loop = asyncio.get_event_loop() + self._price: Decimal = 0 + self._update_interval = 30.0 if (update_interval is None or update_interval < 1) else update_interval + self._fetch_trend_task: Optional[asyncio.Task] = None + self._market_trend = None + self._last_check = 0 + self._check_expiry = check_expiry + self._expire_time = 300 if (expire_time is None or expire_time < 1) else (expire_time * 60) # Seconds + self._use_indicator_time = use_indicator_time + + @property + def name(self): + return self._api_name + + @property + def health_check_endpoint(self): + return self._api_url + + def _http_client(self) -> aiohttp.ClientSession: + if self._shared_client is None: + self._shared_client = aiohttp.ClientSession() + return self._shared_client + + async def check_network(self) -> NetworkStatus: + client = self._http_client() + async with client.request("GET", + self.health_check_endpoint, + params=self._api_auth_params) as resp: + status_text = await resp.text() + if resp.status != 200: + raise Exception(f"Market Indicator Feed {self.name} server error: {status_text}") + return NetworkStatus.CONNECTED + + def trend_is_up(self) -> bool: + if not self._check_expiry or self._last_check > int(time.time() - self._expire_time): + if self._market_trend is True: + return True + return False + return None + + def trend_is_down(self) -> bool: + if not self._check_expiry or self._last_check > int(time.time() - self._expire_time): + if self._market_trend is False: + return True + return False + return None + + async def fetch_trend_loop(self): + while True: + try: + await self.fetch_trend() + except asyncio.CancelledError: + raise + except Exception: + self.logger().network(f"Error fetching a new price from {self._api_url}.", exc_info=True, + app_warning_msg="Couldn't fetch newest price from CustomAPI. " + "Check network connection.") + + await asyncio.sleep(self._update_interval) + + async def fetch_trend(self): + try: + rjson = {} + client = self._http_client() + async with client.request("GET", + self._api_url, + params=self._api_auth_params) as resp: + if resp.status != 200: + resp_text = await resp.text() + raise Exception(f"Custom API Feed {self.name} server error: {resp_text}") + rjson = await resp.json() + respKeys = list(rjson.keys()) + if 'market_indicator' in respKeys: + self._market_trend = True if rjson['market_indicator'] == 'up' else False + time_key = None + if "timestamp" in respKeys and self._use_indicator_time: + time_key = "timestamp" + elif "time" in respKeys and self._use_indicator_time: + time_key = "time" + self._last_check = int(time.time()) + if time_key is not None: + try: + self._last_check = int(rjson[time_key]) + except Exception: + pass + self._ready_event.set() + except Exception as e: + raise Exception(f"Custom API Feed {self.name} server error: {e}") + + async def start_network(self): + await self.stop_network() + self._fetch_trend_task = safe_ensure_future(self.fetch_trend_loop()) + + async def stop_network(self): + if self._fetch_trend_task is not None: + self._fetch_trend_task.cancel() + self._fetch_trend_task = None + + def start(self): + NetworkBase.start(self) + + def stop(self): + NetworkBase.stop(self) diff --git a/hummingbot/strategy/the_money_pit/__init__.py b/hummingbot/strategy/the_money_pit/__init__.py new file mode 100644 index 0000000000..ffd4cabff9 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +from .the_money_pit import TheMoneyPitStrategy +from .asset_price_delegate import AssetPriceDelegate +from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate +from .api_asset_price_delegate import APIAssetPriceDelegate +from .inventory_cost_price_delegate import InventoryCostPriceDelegate +from .market_indicator_delegate import MarketIndicatorDelegate +__all__ = [ + TheMoneyPitStrategy, + AssetPriceDelegate, + OrderBookAssetPriceDelegate, + APIAssetPriceDelegate, + InventoryCostPriceDelegate, + MarketIndicatorDelegate, +] diff --git a/hummingbot/strategy/the_money_pit/api_asset_price_delegate.pxd b/hummingbot/strategy/the_money_pit/api_asset_price_delegate.pxd new file mode 100644 index 0000000000..c37fb04d40 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/api_asset_price_delegate.pyx b/hummingbot/strategy/the_money_pit/api_asset_price_delegate.pyx new file mode 100644 index 0000000000..5134db639e --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/asset_price_delegate.pxd b/hummingbot/strategy/the_money_pit/asset_price_delegate.pxd new file mode 100644 index 0000000000..af6a7bf0fd --- /dev/null +++ b/hummingbot/strategy/the_money_pit/asset_price_delegate.pxd @@ -0,0 +1,3 @@ + +cdef class AssetPriceDelegate: + cdef object c_get_mid_price(self) diff --git a/hummingbot/strategy/the_money_pit/asset_price_delegate.pyx b/hummingbot/strategy/the_money_pit/asset_price_delegate.pyx new file mode 100644 index 0000000000..c68f3d665f --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/data_types.py b/hummingbot/strategy/the_money_pit/data_types.py new file mode 100644 index 0000000000..4a8c1f5d04 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/inventory_cost_price_delegate.py b/hummingbot/strategy/the_money_pit/inventory_cost_price_delegate.py new file mode 100644 index 0000000000..5c411c506c --- /dev/null +++ b/hummingbot/strategy/the_money_pit/inventory_cost_price_delegate.py @@ -0,0 +1,70 @@ +from decimal import Decimal, InvalidOperation +from typing import Optional + +from hummingbot.core.event.events import OrderFilledEvent, TradeType +from hummingbot.model.inventory_cost import InventoryCost +from hummingbot.model.sql_connection_manager import SQLConnectionManager + +s_decimal_0 = Decimal("0") + + +class InventoryCostPriceDelegate: + def __init__(self, sql: SQLConnectionManager, trading_pair: str) -> None: + self.base_asset, self.quote_asset = trading_pair.split("-") + self._session = sql.get_shared_session() + + @property + def ready(self) -> bool: + return True + + def get_price(self) -> Optional[Decimal]: + record = InventoryCost.get_record( + self._session, self.base_asset, self.quote_asset + ) + + if record is None or record.base_volume is None or record.base_volume is None: + return None + + try: + price = record.quote_volume / record.base_volume + except InvalidOperation: + # decimal.InvalidOperation: [] - both volumes are 0 + return None + return Decimal(price) + + def process_order_fill_event(self, fill_event: OrderFilledEvent) -> None: + base_asset, quote_asset = fill_event.trading_pair.split("-") + quote_volume = fill_event.amount * fill_event.price + base_volume = fill_event.amount + + for fee_asset, fee_amount in fill_event.trade_fee.flat_fees: + if fill_event.trade_type == TradeType.BUY: + if fee_asset == base_asset: + base_volume -= fee_amount + elif fee_asset == quote_asset: + quote_volume += fee_amount + else: + # Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity + base_volume /= 1 + fill_event.trade_fee.percent + else: + if fee_asset == base_asset: + base_volume += fee_amount + elif fee_asset == quote_asset: + # TODO: with new logic, this quote volume adjustment does not impacts anything + quote_volume -= fee_amount + else: + # Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity + base_volume /= 1 + fill_event.trade_fee.percent + + if fill_event.trade_type == TradeType.SELL: + record = InventoryCost.get_record(self._session, base_asset, quote_asset) + if not record: + raise RuntimeError("Sold asset without having inventory price set. This should not happen.") + + # We're keeping initial buy price intact. Profits are not changing inventory price intentionally. + quote_volume = -(Decimal(record.quote_volume / record.base_volume) * base_volume) + base_volume = -base_volume + + InventoryCost.add_volume( + self._session, base_asset, quote_asset, base_volume, quote_volume + ) diff --git a/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pxd b/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pxd new file mode 100644 index 0000000000..57f5c983d8 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pxd @@ -0,0 +1,5 @@ +cdef object c_calculate_bid_ask_ratios_from_base_asset_ratio(double base_asset_amount, + double quote_asset_amount, + double price, + double target_base_asset_ratio, + double base_asset_range) diff --git a/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pyx b/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pyx new file mode 100644 index 0000000000..758e58ea61 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/inventory_skew_calculator.pyx @@ -0,0 +1,57 @@ +from decimal import Decimal +import numpy as np + +from .data_types import InventorySkewBidAskRatios + +decimal_0 = Decimal(0) +decimal_1 = Decimal(1) +decimal_2 = Decimal(2) + + +def calculate_total_order_size(order_start_size: Decimal, order_step_size: Decimal = decimal_0, + order_levels: int = 1) -> Decimal: + order_levels_decimal = order_levels + return (decimal_2 * + (order_levels_decimal * order_start_size + + order_levels_decimal * (order_levels_decimal - decimal_1) / decimal_2 * order_step_size + ) + ) + + +def calculate_bid_ask_ratios_from_base_asset_ratio( + base_asset_amount: float, quote_asset_amount: float, price: float, + target_base_asset_ratio: float, base_asset_range: float) -> InventorySkewBidAskRatios: + return c_calculate_bid_ask_ratios_from_base_asset_ratio(base_asset_amount, + quote_asset_amount, + price, + target_base_asset_ratio, + base_asset_range) + + +cdef object c_calculate_bid_ask_ratios_from_base_asset_ratio( + double base_asset_amount, double quote_asset_amount, double price, + double target_base_asset_ratio, double base_asset_range): + cdef: + double total_portfolio_value = base_asset_amount * price + quote_asset_amount + + if total_portfolio_value <= 0.0 or base_asset_range <= 0.0: + return InventorySkewBidAskRatios(0.0, 0.0) + + cdef: + double base_asset_value = base_asset_amount * price + double base_asset_range_value = min(base_asset_range * price, total_portfolio_value * 0.5) + double target_base_asset_value = total_portfolio_value * target_base_asset_ratio + double left_base_asset_value_limit = max(target_base_asset_value - base_asset_range_value, 0.0) + double right_base_asset_value_limit = target_base_asset_value + base_asset_range_value + double left_inventory_ratio = np.interp(base_asset_value, + [left_base_asset_value_limit, target_base_asset_value], + [0.0, 0.5]) + double right_inventory_ratio = np.interp(base_asset_value, + [target_base_asset_value, right_base_asset_value_limit], + [0.5, 1.0]) + double bid_adjustment = (np.interp(left_inventory_ratio, [0, 0.5], [2.0, 1.0]) + if base_asset_value < target_base_asset_value + else np.interp(right_inventory_ratio, [0.5, 1], [1.0, 0.0])) + double ask_adjustment = 2.0 - bid_adjustment + + return InventorySkewBidAskRatios(bid_adjustment, ask_adjustment) diff --git a/hummingbot/strategy/the_money_pit/market_indicator_delegate.pxd b/hummingbot/strategy/the_money_pit/market_indicator_delegate.pxd new file mode 100644 index 0000000000..5dc5ffd7f8 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/market_indicator_delegate.pxd @@ -0,0 +1,4 @@ +cdef class MarketIndicatorDelegate: + cdef object _market_indicator_feed + cdef object c_trend_is_up(self) + cdef object c_trend_is_down(self) diff --git a/hummingbot/strategy/the_money_pit/market_indicator_delegate.pyx b/hummingbot/strategy/the_money_pit/market_indicator_delegate.pyx new file mode 100644 index 0000000000..610da58ced --- /dev/null +++ b/hummingbot/strategy/the_money_pit/market_indicator_delegate.pyx @@ -0,0 +1,38 @@ +from hummingbot.data_feed.market_indicator_data_feed import MarketIndicatorDataFeed, NetworkStatus + +cdef class MarketIndicatorDelegate: + def __init__(self, + api_url: str, + api_key: str, + update_interval: float = None, + check_expiry: bool = False, + expire_time: int = None, + use_indicator_time: bool = False): + super().__init__() + self._market_indicator_feed = MarketIndicatorDataFeed(api_url=api_url, + api_key=api_key, + update_interval=update_interval, + check_expiry=check_expiry, + expire_time=expire_time, + use_indicator_time=use_indicator_time) + self._market_indicator_feed.start() + + def trend_is_up(self) -> bool: + return self.c_trend_is_up() + + def trend_is_down(self) -> bool: + return self.c_trend_is_down() + + cdef object c_trend_is_up(self): + return self._market_indicator_feed.trend_is_up() + + cdef object c_trend_is_down(self): + return self._market_indicator_feed.trend_is_down() + + @property + def ready(self) -> bool: + return self._market_indicator_feed.network_status == NetworkStatus.CONNECTED + + @property + def market_indicator_feed(self) -> MarketIndicatorDataFeed: + return self._market_indicator_feed diff --git a/hummingbot/strategy/the_money_pit/order_book_asset_price_delegate.pxd b/hummingbot/strategy/the_money_pit/order_book_asset_price_delegate.pxd new file mode 100644 index 0000000000..e787cf878c --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/order_book_asset_price_delegate.pyx b/hummingbot/strategy/the_money_pit/order_book_asset_price_delegate.pyx new file mode 100644 index 0000000000..0383401698 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/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/the_money_pit/start.py b/hummingbot/strategy/the_money_pit/start.py new file mode 100644 index 0000000000..e77ecbac34 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/start.py @@ -0,0 +1,164 @@ +from typing import ( + List, + Tuple, +) + +from hummingbot.client.hummingbot_application import HummingbotApplication +from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple +from hummingbot.strategy.the_money_pit import ( + TheMoneyPitStrategy, + OrderBookAssetPriceDelegate, + APIAssetPriceDelegate, + InventoryCostPriceDelegate, + MarketIndicatorDelegate, +) +from hummingbot.strategy.the_money_pit.the_money_pit_config_map import the_money_pit_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 + max_order_age = c_map.get("max_order_age").value + bid_spread = c_map.get("bid_spread").value / Decimal('100') + ask_spread = c_map.get("ask_spread").value / Decimal('100') + minimum_spread = c_map.get("minimum_spread").value / Decimal('100') + price_ceiling = c_map.get("price_ceiling").value + price_floor = c_map.get("price_floor").value + ping_pong_enabled = c_map.get("ping_pong_enabled").value + order_levels = c_map.get("order_levels").value + order_level_amount = c_map.get("order_level_amount").value + order_level_spread = c_map.get("order_level_spread").value / Decimal('100') + exchange = c_map.get("exchange").value.lower() + raw_trading_pair = c_map.get("market").value + inventory_skew_enabled = c_map.get("inventory_skew_enabled").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') + inventory_range_multiplier = c_map.get("inventory_range_multiplier").value + filled_order_delay = c_map.get("filled_order_delay").value + hanging_orders_enabled = c_map.get("hanging_orders_enabled").value + hanging_orders_cancel_pct = c_map.get("hanging_orders_cancel_pct").value / Decimal('100') + order_optimization_enabled = c_map.get("order_optimization_enabled").value + ask_order_optimization_depth = c_map.get("ask_order_optimization_depth").value + bid_order_optimization_depth = c_map.get("bid_order_optimization_depth").value + 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 + 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) + inventory_cost_price_delegate = None + if price_type == "inventory_cost": + db = HummingbotApplication.main_application().trade_fill_db + inventory_cost_price_delegate = InventoryCostPriceDelegate(db, trading_pair) + take_if_crossed = c_map.get("take_if_crossed").value + trade_gain_enabled = c_map.get("trade_gain_enabled").value + trade_gain_hours = c_map.get("trade_gain_hours").value + trade_gain_trades = c_map.get("trade_gain_trades").value + trade_gain_allowed_loss = c_map.get("trade_gain_allowed_loss").value / Decimal('100') + trade_gain_profit_wanted = c_map.get("trade_gain_profit_wanted").value / Decimal('100') + trade_gain_ownside_enabled = c_map.get("trade_gain_ownside_enabled").value + trade_gain_ownside_allowedloss = c_map.get("trade_gain_ownside_allowedloss").value / Decimal('100') + trade_gain_careful_enabled = c_map.get("trade_gain_careful_enabled").value + trade_gain_careful_limittrades = c_map.get("trade_gain_careful_limittrades").value + trade_gain_careful_hours = c_map.get("trade_gain_careful_hours").value + trade_gain_initial_max_buy = c_map.get("trade_gain_initial_max_buy").value + trade_gain_initial_min_sell = c_map.get("trade_gain_initial_min_sell").value + trade_gain_profit_selloff = c_map.get("trade_gain_profit_selloff").value / Decimal('100') + trade_gain_profit_buyin = c_map.get("trade_gain_profit_buyin").value / Decimal('100') + + market_indicator_enabled = c_map.get("market_indicator_enabled").value + market_indicator_delegate = None + market_indicator_reduce_orders_to_pct = c_map.get("market_indicator_reduce_orders_to_pct").value / Decimal('100') + market_indicator_allow_profitable = c_map.get("market_indicator_allow_profitable").value + if market_indicator_enabled is True: + indicator_url = c_map.get("market_indicator_url").value + indicator_key = c_map.get("market_indicator_apikey").value + indicator_refresh_time = c_map.get("market_indicator_refresh_time").value + indicator_disable_expired = c_map.get("market_indicator_disable_expired").value + indicator_expiry = c_map.get("market_indicator_expiry_minutes").value + indicator_use_time = c_map.get("market_indicator_use_apitime").value + market_indicator_delegate = MarketIndicatorDelegate(indicator_url, + indicator_key, + indicator_refresh_time, + indicator_disable_expired, + indicator_expiry, + indicator_use_time) + + strategy_logging_options = TheMoneyPitStrategy.OPTION_LOG_ALL + + self.strategy = TheMoneyPitStrategy( + market_info=MarketTradingPairTuple(*maker_data), + bid_spread=bid_spread, + ask_spread=ask_spread, + order_levels=order_levels, + order_amount=order_amount, + order_level_spread=order_level_spread, + order_level_amount=order_level_amount, + inventory_skew_enabled=inventory_skew_enabled, + inventory_target_base_pct=inventory_target_base_pct, + inventory_range_multiplier=inventory_range_multiplier, + filled_order_delay=filled_order_delay, + hanging_orders_enabled=hanging_orders_enabled, + order_refresh_time=order_refresh_time, + max_order_age = max_order_age, + order_optimization_enabled=order_optimization_enabled, + ask_order_optimization_depth=ask_order_optimization_depth, + bid_order_optimization_depth=bid_order_optimization_depth, + add_transaction_costs_to_orders=add_transaction_costs_to_orders, + logging_options=strategy_logging_options, + asset_price_delegate=asset_price_delegate, + inventory_cost_price_delegate=inventory_cost_price_delegate, + market_indicator_delegate=market_indicator_delegate, + market_indicator_reduce_orders_to_pct=market_indicator_reduce_orders_to_pct, + market_indicator_allow_profitable=market_indicator_allow_profitable, + price_type=price_type, + take_if_crossed=take_if_crossed, + trade_gain_enabled=trade_gain_enabled, + trade_gain_hours=trade_gain_hours, + trade_gain_trades=trade_gain_trades, + trade_gain_allowed_loss=trade_gain_allowed_loss, + trade_gain_profit_wanted=trade_gain_profit_wanted, + trade_gain_ownside_enabled=trade_gain_ownside_enabled, + trade_gain_ownside_allowedloss=trade_gain_ownside_allowedloss, + trade_gain_careful_enabled=trade_gain_careful_enabled, + trade_gain_careful_limittrades=trade_gain_careful_limittrades, + trade_gain_careful_hours=trade_gain_careful_hours, + trade_gain_initial_max_buy=trade_gain_initial_max_buy, + trade_gain_initial_min_sell=trade_gain_initial_min_sell, + trade_gain_profit_selloff=trade_gain_profit_selloff, + trade_gain_profit_buyin=trade_gain_profit_buyin, + price_ceiling=price_ceiling, + price_floor=price_floor, + ping_pong_enabled=ping_pong_enabled, + hanging_orders_cancel_pct=hanging_orders_cancel_pct, + order_refresh_tolerance_pct=order_refresh_tolerance_pct, + minimum_spread=minimum_spread, + hb_app_notification=True, + order_override={} if order_override is None else order_override, + ) + except Exception as e: + self._notify(str(e)) + self.logger().error("Unknown error during initialization.", exc_info=True) diff --git a/hummingbot/strategy/the_money_pit/the_money_pit.pxd b/hummingbot/strategy/the_money_pit/the_money_pit.pxd new file mode 100644 index 0000000000..a4c37878e5 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/the_money_pit.pxd @@ -0,0 +1,106 @@ +# distutils: language=c++ + +from libc.stdint cimport int64_t +from hummingbot.strategy.strategy_base cimport StrategyBase + + +cdef class TheMoneyPitStrategy(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_target_base_pct_restore + object _inventory_range_multiplier + object _inventory_range_multiplier_restore + 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 _market_indicator_delegate + object _market_indicator_reduce_orders_to_pct + object _market_indicator_allow_profitable + object _price_type + bint _take_if_crossed + bint _trade_gain_enabled + object _trade_gain_hours + int _trade_gain_trades + object _trade_gain_allowed_loss + object _trade_gain_profit_wanted + bint _trade_gain_ownside_enabled + object _trade_gain_ownside_allowedloss + bint _trade_gain_careful_enabled + int _trade_gain_careful_limittrades + object _trade_gain_careful_hours + object _trade_gain_initial_max_buy + object _trade_gain_initial_min_sell + object _trade_gain_pricethresh_buy + object _trade_gain_pricethresh_sell + object _trade_gain_pricethresh_buyin + object _trade_gain_profit_selloff + object _trade_gain_profit_buyin + object _trade_gain_profitability + bint _trade_gain_dump_it + 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 + double _start_timestamp + double _pnl_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 + + 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_order_size_modifiers(self, object proposal) + cdef c_apply_inventory_skew(self, object proposal) + cdef c_apply_budget_constraint(self, object proposal) + cdef c_apply_profit_constraint(self, object proposal) + cdef c_apply_indicator_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_cancel_orders_below_min_spread(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) diff --git a/hummingbot/strategy/the_money_pit/the_money_pit.pyx b/hummingbot/strategy/the_money_pit/the_money_pit.pyx new file mode 100644 index 0000000000..cd8b898a6d --- /dev/null +++ b/hummingbot/strategy/the_money_pit/the_money_pit.pyx @@ -0,0 +1,1700 @@ +from decimal import Decimal +import logging +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.trade import Trade +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.connector.markets_recorder import MarketsRecorder +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 .the_money_pit_order_tracker import TheMoneyPitOrderTracker + +from .asset_price_delegate cimport AssetPriceDelegate +from .asset_price_delegate import AssetPriceDelegate +from .inventory_skew_calculator cimport c_calculate_bid_ask_ratios_from_base_asset_ratio +from .inventory_skew_calculator import calculate_total_order_size +from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate +from .inventory_cost_price_delegate import InventoryCostPriceDelegate +from .market_indicator_delegate import MarketIndicatorDelegate +from .market_indicator_delegate cimport MarketIndicatorDelegate + + +NaN = float("nan") +s_decimal_zero = Decimal(0) +s_decimal_neg_one = Decimal(-1) +pmm_logger = None + + +cdef class TheMoneyPitStrategy(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, + bid_spread: Decimal, + ask_spread: Decimal, + order_amount: Decimal, + order_levels: int = 1, + order_level_spread: Decimal = s_decimal_zero, + order_level_amount: Decimal = s_decimal_zero, + 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_skew_enabled: bool = False, + inventory_target_base_pct: Decimal = s_decimal_zero, + inventory_range_multiplier: Decimal = s_decimal_zero, + hanging_orders_enabled: bool = False, + hanging_orders_cancel_pct: Decimal = Decimal("0.1"), + order_optimization_enabled: bool = False, + ask_order_optimization_depth: Decimal = s_decimal_zero, + bid_order_optimization_depth: Decimal = s_decimal_zero, + add_transaction_costs_to_orders: bool = False, + asset_price_delegate: AssetPriceDelegate = None, + inventory_cost_price_delegate: InventoryCostPriceDelegate = None, + market_indicator_delegate: MarketIndicatorDelegate = None, + market_indicator_reduce_orders_to_pct: Decimal = s_decimal_zero, + market_indicator_allow_profitable: bool = False, + price_type: str = "mid_price", + take_if_crossed: bool = False, + trade_gain_enabled: bool = False, + trade_gain_hours: Decimal = Decimal(4), + trade_gain_trades: int = 1, + trade_gain_allowed_loss: Decimal = Decimal("0.2"), + trade_gain_profit_wanted: Decimal = Decimal("0.2"), + trade_gain_ownside_enabled: bool = False, + trade_gain_ownside_allowedloss: Decimal = Decimal("0.2"), + trade_gain_careful_enabled: bool = False, + trade_gain_careful_limittrades: int = 3, + trade_gain_careful_hours: Decimal = Decimal(4), + trade_gain_initial_max_buy: Decimal = s_decimal_zero, + trade_gain_initial_min_sell: Decimal = s_decimal_zero, + trade_gain_profit_selloff: Decimal = s_decimal_zero, + trade_gain_profit_buyin: Decimal = s_decimal_zero, + 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, + minimum_spread: Decimal = Decimal(0), + hb_app_notification: bool = False, + order_override: Dict[str, List[str]] = {}, + ): + + if price_ceiling != s_decimal_neg_one and price_ceiling < price_floor: + raise ValueError("Parameter price_ceiling cannot be lower than price_floor.") + + super().__init__() + self._sb_order_tracker = TheMoneyPitOrderTracker() + self._market_info = market_info + self._bid_spread = bid_spread + self._ask_spread = ask_spread + self._minimum_spread = minimum_spread + self._order_amount = order_amount + self._order_levels = order_levels + self._buy_levels = order_levels + self._sell_levels = order_levels + self._order_level_spread = order_level_spread + self._order_level_amount = order_level_amount + 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_skew_enabled = inventory_skew_enabled + self._inventory_target_base_pct = inventory_target_base_pct + self._inventory_target_base_pct_restore = inventory_target_base_pct + self._inventory_range_multiplier = inventory_range_multiplier + self._inventory_range_multiplier_restore = inventory_range_multiplier + self._hanging_orders_enabled = hanging_orders_enabled + self._hanging_orders_cancel_pct = hanging_orders_cancel_pct + self._order_optimization_enabled = order_optimization_enabled + self._ask_order_optimization_depth = ask_order_optimization_depth + self._bid_order_optimization_depth = bid_order_optimization_depth + self._add_transaction_costs_to_orders = add_transaction_costs_to_orders + self._asset_price_delegate = asset_price_delegate + self._inventory_cost_price_delegate = inventory_cost_price_delegate + self._market_indicator_delegate = market_indicator_delegate + self._market_indicator_reduce_orders_to_pct = market_indicator_reduce_orders_to_pct + self._market_indicator_allow_profitable = market_indicator_allow_profitable + self._price_type = self.get_price_type(price_type) + self._take_if_crossed = take_if_crossed + self._trade_gain_enabled = trade_gain_enabled + self._trade_gain_hours = trade_gain_hours + self._trade_gain_trades = trade_gain_trades + self._trade_gain_allowed_loss = trade_gain_allowed_loss + self._trade_gain_profit_wanted = trade_gain_profit_wanted + self._trade_gain_ownside_enabled = trade_gain_ownside_enabled + self._trade_gain_ownside_allowedloss = trade_gain_ownside_allowedloss + self._trade_gain_careful_enabled = trade_gain_careful_enabled + self._trade_gain_careful_limittrades = trade_gain_careful_limittrades + self._trade_gain_careful_hours = trade_gain_careful_hours + self._trade_gain_initial_max_buy = trade_gain_initial_max_buy + self._trade_gain_initial_min_sell = trade_gain_initial_min_sell + self._trade_gain_profit_selloff = trade_gain_profit_selloff + self._trade_gain_profit_buyin = trade_gain_profit_buyin + 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._start_timestamp = 0 + self._pnl_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._trade_gain_pricethresh_buy = s_decimal_zero + self._trade_gain_pricethresh_sell = s_decimal_zero + self._trade_gain_dump_it = False + self._trade_gain_profitability = s_decimal_zero + + self.c_add_markets([market_info.market]) + + 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 trade_gain_enabled(self) -> bool: + return self._trade_gain_enabled + + @trade_gain_enabled.setter + def trade_gain_enabled(self, value: bool): + self._trade_gain_enabled = value + + @property + def trade_gain_hours(self) -> Decimal: + return self._trade_gain_hours + + @trade_gain_hours.setter + def trade_gain_hours(self, value: Decimal): + self._trade_gain_hours = value + + @property + def trade_gain_trades(self) -> int: + return self._trade_gain_trades + + @trade_gain_trades.setter + def trade_gain_trades(self, value: int): + self._trade_gain_trades = value + + @property + def trade_gain_allowed_loss(self) -> Decimal: + return self._trade_gain_allowed_loss + + @trade_gain_allowed_loss.setter + def trade_gain_allowed_loss(self, value: Decimal): + self._trade_gain_allowed_loss = value + + @property + def trade_gain_profit_wanted(self) -> Decimal: + return self._trade_gain_profit_wanted + + @trade_gain_profit_wanted.setter + def trade_gain_profit_wanted(self, value: Decimal): + self._trade_gain_profit_wanted = value + + @property + def trade_gain_ownside_enabled(self) -> bool: + return self._trade_gain_ownside_enabled + + @trade_gain_ownside_enabled.setter + def trade_gain_ownside_enabled(self, value: bool): + self._trade_gain_ownside_enabled = value + + @property + def trade_gain_ownside_allowedloss(self) -> Decimal: + return self._trade_gain_ownside_allowedloss + + @trade_gain_ownside_allowedloss.setter + def trade_gain_ownside_allowedloss(self, value: Decimal): + self._trade_gain_ownside_allowedloss = value + + @property + def trade_gain_careful_enabled(self) -> bool: + return self._trade_gain_careful_enabled + + @trade_gain_careful_enabled.setter + def trade_gain_careful_enabled(self, value: bool): + self._trade_gain_careful_enabled = value + + @property + def trade_gain_careful_limittrades(self) -> int: + return self._trade_gain_careful_limittrades + + @trade_gain_careful_limittrades.setter + def trade_gain_careful_limittrades(self, value: int): + self._trade_gain_careful_limittrades = value + + @property + def trade_gain_careful_hours(self) -> Decimal: + return self._trade_gain_careful_hours + + @trade_gain_careful_hours.setter + def trade_gain_careful_hours(self, value: Decimal): + self._trade_gain_careful_hours = value + + @property + def trade_gain_initial_max_buy(self) -> Decimal: + return self._trade_gain_initial_max_buy + + @trade_gain_initial_max_buy.setter + def trade_gain_initial_max_buy(self, value: Decimal): + self._trade_gain_initial_max_buy = value + + @property + def trade_gain_initial_min_sell(self) -> Decimal: + return self._trade_gain_initial_min_sell + + @trade_gain_initial_min_sell.setter + def trade_gain_initial_min_sell(self, value: Decimal): + self._trade_gain_initial_min_sell = value + + @property + def trade_gain_profit_selloff(self) -> Decimal: + return self._trade_gain_profit_selloff + + @trade_gain_profit_selloff.setter + def trade_gain_profit_selloff(self, value: Decimal): + self._trade_gain_profit_selloff = value + + @property + def trade_gain_profit_buyin(self) -> Decimal: + return self._trade_gain_profit_buyin + + @trade_gain_profit_buyin.setter + def trade_gain_profit_buyin(self, value: Decimal): + self._trade_gain_profit_buyin = value + + @property + def trade_gain_pricethresh_buy(self) -> Decimal: + return self._trade_gain_pricethresh_buy + + @trade_gain_pricethresh_buy.setter + def trade_gain_pricethresh_buy(self, value: Decimal): + self._trade_gain_pricethresh_buy = value + + @property + def trade_gain_pricethresh_sell(self) -> Decimal: + return self._trade_gain_pricethresh_sell + + @trade_gain_pricethresh_sell.setter + def trade_gain_pricethresh_sell(self, value: Decimal): + self._trade_gain_pricethresh_sell = value + + @property + def markets_recorder(self) -> MarketsRecorder: + from hummingbot.client.hummingbot_application import HummingbotApplication + return HummingbotApplication.main_application().markets_recorder + + @property + def strategy_file_name(self) -> str: + from hummingbot.client.hummingbot_application import HummingbotApplication + return HummingbotApplication.main_application().strategy_file_name + + @property + def trades_history(self) -> List[LimitOrder]: + return self.markets_recorder.get_trades_for_config(self.strategy_file_name, + 2000) + + @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 market_indicator_delegate(self) -> MarketIndicatorDelegate: + return self._market_indicator_delegate + + @market_indicator_delegate.setter + def market_indicator_delegate(self, value: MarketIndicatorDelegate): + self._market_indicator_delegate = value + + @property + def market_indicator_reduce_orders_to_pct(self) -> Decimal: + return self._market_indicator_reduce_orders_to_pct + + @market_indicator_reduce_orders_to_pct.setter + def market_indicator_reduce_orders_to_pct(self, value: Decimal): + self._market_indicator_reduce_orders_to_pct = value + + @property + def market_indicator_allow_profitable(self) -> bool: + return self._market_indicator_allow_profitable + + @market_indicator_allow_profitable.setter + def market_indicator_allow_profitable(self, value: bool): + self._market_indicator_allow_profitable = value + + @property + def order_tracker(self): + return self._sb_order_tracker + + def inventory_skew_stats_data_frame(self) -> Optional[pd.DataFrame]: + cdef: + ExchangeBase market = self._market_info.market + + price = self.get_price() + base_asset_amount, quote_asset_amount = self.c_get_adjusted_available_balance(self.active_orders) + total_order_size = calculate_total_order_size(self._order_amount, self._order_level_amount, self._order_levels) + + base_asset_value = base_asset_amount * price + quote_asset_value = quote_asset_amount / price if price > s_decimal_zero else s_decimal_zero + total_value = base_asset_amount + quote_asset_value + total_value_in_quote = (base_asset_amount * price) + quote_asset_amount + + base_asset_ratio = (base_asset_amount / total_value + if total_value > s_decimal_zero + else s_decimal_zero) + quote_asset_ratio = Decimal("1") - base_asset_ratio if total_value > 0 else 0 + target_base_ratio = self._inventory_target_base_pct + inventory_range_multiplier = self._inventory_range_multiplier + target_base_amount = (total_value * target_base_ratio + if price > s_decimal_zero + else s_decimal_zero) + target_base_amount_in_quote = target_base_ratio * total_value_in_quote + target_quote_amount = (1 - target_base_ratio) * total_value_in_quote + + base_asset_range = total_order_size * self._inventory_range_multiplier + base_asset_range = min(base_asset_range, total_value * Decimal("0.5")) + high_water_mark = target_base_amount + base_asset_range + low_water_mark = max(target_base_amount - base_asset_range, s_decimal_zero) + low_water_mark_ratio = (low_water_mark / total_value + if total_value > s_decimal_zero + else s_decimal_zero) + high_water_mark_ratio = (high_water_mark / total_value + if total_value > s_decimal_zero + else s_decimal_zero) + high_water_mark_ratio = min(1.0, high_water_mark_ratio) + total_order_size_ratio = (self._order_amount * Decimal("2") / total_value + if total_value > s_decimal_zero + else s_decimal_zero) + bid_ask_ratios = c_calculate_bid_ask_ratios_from_base_asset_ratio( + float(base_asset_amount), + float(quote_asset_amount), + float(price), + float(target_base_ratio), + float(base_asset_range) + ) + inventory_skew_df = pd.DataFrame(data=[ + [f"Target Value ({self.quote_asset})", f"{target_base_amount_in_quote:.4f}", + f"{target_quote_amount:.4f}"], + ["Current %", f"{base_asset_ratio:.1%}", f"{quote_asset_ratio:.1%}"], + ["Target %", f"{target_base_ratio:.1%}", f"{1 - target_base_ratio:.1%}"], + ["Inventory Range", f"{low_water_mark_ratio:.1%} - {high_water_mark_ratio:.1%}", + f"{1 - high_water_mark_ratio:.1%} - {1 - low_water_mark_ratio:.1%}"], + ["Order Adjust %", f"{bid_ask_ratios.bid_ratio:.1%}", f"{bid_ask_ratios.ask_ratio:.1%}"] + ]) + return inventory_skew_df + + 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")]) + + if self.market_indicator_delegate is not None: + trend_status = self.market_indicator_delegate.trend_is_up() + if trend_status is True: + trend_str = "Up" + elif trend_status is False: + trend_str = "Down" + else: + trend_str = "Expired" + trend_name = self.market_indicator_delegate.market_indicator_feed.name + lines.extend(["", " Trend:"] + [f" Market Trend is {trend_str} ({trend_name})"]) + + 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 + self._start_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.") + + 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) + # 4. Apply functions that modify orders size + self.c_apply_order_size_modifiers(proposal) + # 4.A. Apply Trade Gain Rules + if self._trade_gain_enabled: + self.c_apply_profit_constraint(proposal) + # 4.B. Apply Market Indicator Rules + if self._market_indicator_delegate is not None and not self._trade_gain_dump_it: + self.c_apply_indicator_constraint(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() + self.c_cancel_orders_below_min_spread() + 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 object c_create_base_proposal(self): + cdef: + ExchangeBase market = self._market_info.market + list buys = [] + list sells = [] + + buy_reference_price = sell_reference_price = self.get_price() + + if self._inventory_cost_price_delegate is not None: + inventory_cost_price = self._inventory_cost_price_delegate.get_price() + if inventory_cost_price is not None: + # Only limit sell price. Buy are always allowed. + sell_reference_price = max(inventory_cost_price, sell_reference_price) + else: + base_balance = float(market.get_balance(self._market_info.base_asset)) + if base_balance > 0: + raise RuntimeError("Initial inventory price is not set while inventory_cost feature is active.") + + # First to check if a customized order override is configured, otherwise the proposal will be created according + # to order spread, amount, and levels setting. + order_override = self._order_override + if order_override is not None and len(order_override) > 0: + for key, value in order_override.items(): + if str(value[0]) in ["buy", "sell"]: + if str(value[0]) == "buy": + price = buy_reference_price * (Decimal("1") - Decimal(str(value[1])) / Decimal("100")) + price = market.c_quantize_order_price(self.trading_pair, price) + size = Decimal(str(value[2])) + size = market.c_quantize_order_amount(self.trading_pair, size) + if size > 0 and price > 0: + buys.append(PriceSize(price, size)) + elif str(value[0]) == "sell": + price = sell_reference_price * (Decimal("1") + Decimal(str(value[1])) / Decimal("100")) + price = market.c_quantize_order_price(self.trading_pair, price) + size = Decimal(str(value[2])) + size = market.c_quantize_order_amount(self.trading_pair, size) + if size > 0 and price > 0: + sells.append(PriceSize(price, size)) + else: + for level in range(0, self._buy_levels): + price = buy_reference_price * (Decimal("1") - self._bid_spread - (level * self._order_level_spread)) + price = market.c_quantize_order_price(self.trading_pair, price) + size = self._order_amount + (self._order_level_amount * level) + size = market.c_quantize_order_amount(self.trading_pair, size) + if size > 0: + buys.append(PriceSize(price, size)) + for level in range(0, self._sell_levels): + price = sell_reference_price * (Decimal("1") + self._ask_spread + (level * self._order_level_spread)) + price = market.c_quantize_order_price(self.trading_pair, price) + size = self._order_amount + (self._order_level_amount * level) + size = market.c_quantize_order_amount(self.trading_pair, size) + 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_order_size_modifiers(self, object proposal): + if self._inventory_skew_enabled: + self.c_apply_inventory_skew(proposal) + + cdef c_apply_inventory_skew(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + object bid_adj_ratio + object ask_adj_ratio + object size + + base_balance, quote_balance = self.c_get_adjusted_available_balance(self.active_orders) + + total_order_size = calculate_total_order_size(self._order_amount, self._order_level_amount, self._order_levels) + bid_ask_ratios = c_calculate_bid_ask_ratios_from_base_asset_ratio( + float(base_balance), + float(quote_balance), + float(self.get_price()), + float(self._inventory_target_base_pct), + float(total_order_size * self._inventory_range_multiplier) + ) + bid_adj_ratio = Decimal(bid_ask_ratios.bid_ratio) + ask_adj_ratio = Decimal(bid_ask_ratios.ask_ratio) + + for buy in proposal.buys: + size = buy.size * bid_adj_ratio + size = market.c_quantize_order_amount(self.trading_pair, size) + buy.size = size + + for sell in proposal.sells: + size = sell.size * ask_adj_ratio + size = market.c_quantize_order_amount(self.trading_pair, size, sell.price) + sell.size = size + + 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] + + # TRADE TRACKER + cdef c_apply_profit_constraint(self, object proposal): + cdef: + ExchangeBase market = self._market_info.market + int accept_time = int(time.time() - int((self.trade_gain_hours * (60 * 60)))) + int accept_time_careful = int(time.time() - int((self.trade_gain_careful_hours * (60 * 60)))) + int recent_buys = 0 + int recent_buys_cf = 0 + int recent_sells = 0 + int recent_sells_cf = 0 + bint careful_trades = self.trade_gain_careful_enabled + int careful_trades_limit = self.trade_gain_careful_limittrades + int recent_trades_limit = self.trade_gain_trades + object highest_buy_price = s_decimal_zero + object lowest_buy_price = s_decimal_zero + object highest_sell_price = s_decimal_zero + object lowest_sell_price = s_decimal_zero + object buy_margin = self.trade_gain_allowed_loss + Decimal('1') + object buy_margin_on_self = self.trade_gain_ownside_allowedloss + Decimal('1') + object buy_profit = Decimal('1') - self.trade_gain_profit_wanted + object sell_margin = Decimal('1') - self.trade_gain_allowed_loss + object sell_margin_on_self = Decimal('1') - self.trade_gain_ownside_allowedloss + object sell_profit = self.trade_gain_profit_wanted + Decimal('1') + list trades = self.trades + list trades_history = self.trades_history + object filtered_trades = {} + object current_price = self.get_price() + cdef double next_pnl_cycle = self._current_timestamp + self._order_refresh_time + cdef double buyback_pnl_cycle = self._current_timestamp + (self._order_refresh_time * 10) + + if not self._trade_gain_dump_it or self.trade_gain_profit_buyin == s_decimal_zero: + self.trade_gain_pricethresh_buy = s_decimal_zero + self.trade_gain_pricethresh_sell = s_decimal_zero + + # PnL Targets + if self._pnl_timestamp <= self._current_timestamp and self.trade_gain_profit_selloff > s_decimal_zero: + self._pnl_timestamp = next_pnl_cycle + ks_profitability = self.main_profitability() + if ks_profitability is not None: + rel_profitability = profitability = Decimal(str(ks_profitability)) + if self._trade_gain_profitability != s_decimal_zero: + rel_profitability = profitability - self._trade_gain_profitability + # Check and switch dump-it mode + if not self._trade_gain_dump_it and rel_profitability >= self.trade_gain_profit_selloff: + self.logger().info(f"Hit profit target @ {rel_profitability:.4f}, beginning sell-off.") + self._trade_gain_dump_it = True + self._trade_gain_profitability = profitability + if self._inventory_target_base_pct != Decimal("0.000001"): + self._inventory_target_base_pct_restore = self._inventory_target_base_pct + self._inventory_target_base_pct = Decimal("0.000001") + if self._inventory_range_multiplier != Decimal("0.000001"): + self._inventory_range_multiplier_restore = self._inventory_range_multiplier + self._inventory_range_multiplier = Decimal("0.000001") + if self.trade_gain_profit_buyin > s_decimal_zero: + self.trade_gain_pricethresh_buy = current_price * (Decimal('1') - self.trade_gain_profit_buyin) + # Check and switch buy-back-in mode + elif self._trade_gain_dump_it and (self.trade_gain_profit_buyin > s_decimal_zero and + current_price < self.trade_gain_pricethresh_buy): + self.logger().info(f"Hit buy-back target @ {current_price:.8f}, allowing trades.") + self._trade_gain_dump_it = False + self._trade_gain_profitability = profitability + if self._inventory_target_base_pct == Decimal("0.000001"): + self._inventory_target_base_pct = self._inventory_target_base_pct_restore + if self._inventory_range_multiplier == Decimal("0.000001"): + self._inventory_range_multiplier = self._inventory_range_multiplier_restore + self._pnl_timestamp = buyback_pnl_cycle + + # Order by TS + all_trades = trades + trades_history if len(trades) < 1000 else trades + for trade in all_trades: + trade_ts = int(trade.timestamp) if type(trade) == Trade else int(trade.timestamp / 1000) + trade_side = trade.side.name if type(trade) == Trade else trade.trade_type + if trade_ts > accept_time: + if trade_side == TradeType.SELL.name: + filtered_trades[trade_ts] = trade + if trade_side == TradeType.BUY.name: + filtered_trades[trade_ts] = trade + + # Filter and find trade vals + for trade_ts in sorted(list(filtered_trades.keys()), reverse=True): + trade = filtered_trades[trade_ts] + trade_side = trade.side.name if type(trade) == Trade else trade.trade_type + trade_price = Decimal(str(trade.price)) + if trade_ts > accept_time_careful: + if trade_side == TradeType.SELL.name: + recent_sells_cf += 1 + elif trade_side == TradeType.BUY.name: + recent_buys_cf += 1 + if trade_ts > accept_time: + if trade_side == TradeType.SELL.name: + recent_sells += 1 + if recent_sells <= recent_trades_limit: + if lowest_sell_price == s_decimal_zero or trade_price < lowest_sell_price: + lowest_sell_price = trade_price + if highest_sell_price == s_decimal_zero or trade_price > highest_sell_price: + highest_sell_price = trade_price + elif trade_side == TradeType.BUY.name: + recent_buys += 1 + if recent_buys <= recent_trades_limit: + if lowest_buy_price == s_decimal_zero or trade_price < lowest_buy_price: + lowest_buy_price = trade_price + if highest_buy_price == s_decimal_zero or trade_price > highest_buy_price: + highest_buy_price = trade_price + + if not self._trade_gain_dump_it or self.trade_gain_profit_buyin == s_decimal_zero: + if lowest_sell_price != s_decimal_zero: + self.trade_gain_pricethresh_buy = Decimal(lowest_sell_price * buy_margin) + self.trade_gain_initial_max_buy = s_decimal_zero + elif self.trade_gain_initial_max_buy > s_decimal_zero: + self.trade_gain_pricethresh_buy = self.trade_gain_initial_max_buy + + if highest_buy_price != s_decimal_zero: + self.trade_gain_pricethresh_sell = Decimal(highest_buy_price * sell_margin) + self.trade_gain_initial_min_sell = s_decimal_zero + elif self.trade_gain_initial_min_sell > s_decimal_zero: + self.trade_gain_pricethresh_sell = self.trade_gain_initial_min_sell + + if self.trade_gain_ownside_enabled: + chk_ownside_buy = ((not self._trade_gain_dump_it or + self.trade_gain_profit_buyin == s_decimal_zero) and + lowest_buy_price != s_decimal_zero and + self.trade_gain_initial_max_buy == s_decimal_zero and + (lowest_sell_price == s_decimal_zero or + (lowest_buy_price * buy_margin_on_self) < self.trade_gain_pricethresh_buy)) + if chk_ownside_buy: + self.trade_gain_pricethresh_buy = Decimal(lowest_buy_price * buy_margin_on_self) + + chk_ownside_sell = (highest_sell_price != s_decimal_zero and + self.trade_gain_initial_min_sell == s_decimal_zero and + (highest_buy_price == s_decimal_zero or + (highest_sell_price * sell_margin_on_self) > self.trade_gain_pricethresh_sell)) + if chk_ownside_sell: + self.trade_gain_pricethresh_sell = Decimal(highest_sell_price * sell_margin_on_self) + + # Order Cancel Checks + should_cancel = (self._trade_gain_dump_it or (careful_trades and + recent_sells_cf < 1 and + recent_buys_cf >= careful_trades_limit)) + + for buy in proposal.buys: + # Order Raise Checks + should_raise = (not self._trade_gain_dump_it and + buy.price > self.trade_gain_pricethresh_buy and + self.trade_gain_pricethresh_buy != s_decimal_zero) + if should_raise: + buy_fee = market.c_get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, + buy.size, buy.price) + quote_amount = Decimal((buy.size * buy.price) * (Decimal('1') - buy_fee.percent)) + adjusted_price = Decimal((self.trade_gain_pricethresh_buy - (current_price - buy.price)) * buy_profit) + buy.price = market.c_quantize_order_price(self.trading_pair, adjusted_price) + adjusted_amount = quote_amount / (buy.price) + buy.size = market.c_quantize_order_amount(self.trading_pair, adjusted_amount) + elif should_cancel: + buy.size = s_decimal_zero + + proposal.buys = [o for o in proposal.buys if o.size > 0] + + for sell in proposal.sells: + if sell.price < self.trade_gain_pricethresh_sell and self.trade_gain_pricethresh_sell != s_decimal_zero: + adjusted_price = Decimal((self.trade_gain_pricethresh_sell + (sell.price - current_price)) * sell_profit) + sell.price = market.c_quantize_order_price(self.trading_pair, adjusted_price) + sell.size = market.c_quantize_order_amount(self.trading_pair, sell.size) + elif careful_trades and recent_buys_cf < 1 and recent_sells_cf >= careful_trades_limit: + sell.size = s_decimal_zero + + proposal.sells = [o for o in proposal.sells if o.size > 0] + # TRADE TRACKER + + # TREND TRACKER + cdef c_apply_indicator_constraint(self, object proposal): + cdef: + MarketIndicatorDelegate indicator = self._market_indicator_delegate + object indicator_orders_pct = self._market_indicator_reduce_orders_to_pct + bint market_trend_up + bint market_trend_down + bint allow_profitable = self._market_indicator_allow_profitable + + market_trend_up = indicator.c_trend_is_up() + market_trend_down = indicator.c_trend_is_down() + + for buy in proposal.buys: + if market_trend_up in [None, False]: + buy.size = buy.size * indicator_orders_pct + + proposal.buys = [o for o in proposal.buys if o.size > 0] + + if not allow_profitable or self.trade_gain_pricethresh_sell == s_decimal_zero: + for sell in proposal.sells: + if market_trend_down in [None, False]: + sell.size = sell.size * indicator_orders_pct + + proposal.sells = [o for o in proposal.sells if o.size > 0] + # TREND TRACKER + + 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): + 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 and self._order_refresh_tolerance_pct >= 0: + + 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) + + # Cancel Non-Hanging, Active Orders if Spreads are below minimum_spread + cdef c_cancel_orders_below_min_spread(self): + cdef: + list active_orders = self.market_info_to_active_orders.get(self._market_info, []) + object price = self.get_price() + active_orders = [order for order in active_orders + if order.client_order_id not in self._hanging_order_ids] + for order in active_orders: + negation = -1 if order.is_buy else 1 + if (negation * (order.price - price) / price) < self._minimum_spread: + self.logger().info(f"Order is below minimum spread ({self._minimum_spread})." + f" Cancelling Order: ({'Buy' if order.is_buy else 'Sell'}) " + f"ID - {order.client_order_id}") + 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 main_profitability(self): + from hummingbot.client.hummingbot_application import HummingbotApplication + return HummingbotApplication.main_application().kill_switch._profitability + + 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/the_money_pit/the_money_pit_config_map.py b/hummingbot/strategy/the_money_pit/the_money_pit_config_map.py new file mode 100644 index 0000000000..949e0759af --- /dev/null +++ b/hummingbot/strategy/the_money_pit/the_money_pit_config_map.py @@ -0,0 +1,506 @@ +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, + validate_int +) +from hummingbot.client.settings import ( + required_exchanges, + EXAMPLE_PAIRS, +) +from hummingbot.client.config.global_config_map import ( + using_bamboo_coordinator_mode, + using_exchange +) +from hummingbot.client.config.config_helpers import ( + minimum_order_amount, +) +from typing import Optional + + +def maker_trading_pair_prompt(): + exchange = the_money_pit_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 = the_money_pit_config_map.get("exchange").value + return validate_market_trading_pair(exchange, value) + + +def order_amount_prompt() -> str: + exchange = the_money_pit_config_map["exchange"].value + trading_pair = the_money_pit_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 = the_money_pit_config_map["exchange"].value + trading_pair = the_money_pit_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": + the_money_pit_config_map["price_source_exchange"].value = None + the_money_pit_config_map["price_source_market"].value = None + the_money_pit_config_map["take_if_crossed"].value = None + if value != "custom_api": + the_money_pit_config_map["price_source_custom_api"].value = None + else: + the_money_pit_config_map["price_type"].value = None + + +def price_source_market_prompt() -> str: + external_market = the_money_pit_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 == the_money_pit_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: + the_money_pit_config_map["price_source_market"].value = None + + +def validate_price_source_market(value: str) -> Optional[str]: + market = the_money_pit_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) + + +the_money_pit_config_map = { + "strategy": + ConfigVar(key="strategy", + prompt=None, + default="the_money_pit"), + "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), + "bid_spread": + ConfigVar(key="bid_spread", + prompt="How far away from the mid price do you want to place the " + "first bid order? (Enter 1 to indicate 1%) >>> ", + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True), + "ask_spread": + ConfigVar(key="ask_spread", + prompt="How far away from the mid price do you want to place the " + "first ask order? (Enter 1 to indicate 1%) >>> ", + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + prompt_on_new=True), + "minimum_spread": + ConfigVar(key="minimum_spread", + prompt="At what minimum spread should the bot automatically cancel orders? (Enter 1 for 1%) >>> ", + required_if=lambda: False, + type_str="decimal", + default=Decimal(-100), + validator=lambda v: validate_decimal(v, -100, 100, True)), + "order_refresh_time": + ConfigVar(key="order_refresh_time", + prompt="How often do you want to cancel and replace bids and asks " + "(in seconds)? >>> ", + required_if=lambda: not (using_exchange("radar_relay")() or + (using_exchange("bamboo_relay")() and not using_bamboo_coordinator_mode())), + type_str="float", + validator=lambda v: validate_decimal(v, 0, inclusive=False), + prompt_on_new=True), + "max_order_age": + ConfigVar(key="max_order_age", + prompt="How long do you want to cancel and replace bids and asks " + "with the same price (in seconds)? >>> ", + required_if=lambda: not (using_exchange("radar_relay")() or + (using_exchange("bamboo_relay")() and not using_bamboo_coordinator_mode())), + type_str="float", + default=Decimal("1800"), + validator=lambda v: validate_decimal(v, 0, inclusive=False)), + "order_refresh_tolerance_pct": + ConfigVar(key="order_refresh_tolerance_pct", + prompt="Enter the percent change in price needed to refresh orders at each cycle " + "(Enter 1 to indicate 1%) >>> ", + type_str="decimal", + default=Decimal("0"), + validator=lambda v: validate_decimal(v, -10, 10, inclusive=True)), + "order_amount": + ConfigVar(key="order_amount", + prompt=order_amount_prompt, + type_str="decimal", + validator=validate_order_amount, + prompt_on_new=True), + "price_ceiling": + ConfigVar(key="price_ceiling", + prompt="Enter the price point above which only sell orders will be placed " + "(Enter -1 to deactivate this feature) >>> ", + type_str="decimal", + default=Decimal("-1"), + validator=validate_price_floor_ceiling), + "price_floor": + ConfigVar(key="price_floor", + prompt="Enter the price below which only buy orders will be placed " + "(Enter -1 to deactivate this feature) >>> ", + type_str="decimal", + default=Decimal("-1"), + validator=validate_price_floor_ceiling), + "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), + "order_levels": + ConfigVar(key="order_levels", + prompt="How many orders do you want to place on both sides? >>> ", + type_str="int", + validator=lambda v: validate_int(v, min_value=-1, inclusive=False), + default=1), + "order_level_amount": + ConfigVar(key="order_level_amount", + prompt="How much do you want to increase or decrease the order size for each " + "additional order? (decrease < 0 > increase) >>> ", + required_if=lambda: the_money_pit_config_map.get("order_levels").value > 1, + type_str="decimal", + validator=lambda v: validate_decimal(v), + default=0), + "order_level_spread": + ConfigVar(key="order_level_spread", + prompt="Enter the price increments (as percentage) for subsequent " + "orders? (Enter 1 to indicate 1%) >>> ", + required_if=lambda: the_money_pit_config_map.get("order_levels").value > 1, + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False), + default=Decimal("1")), + "inventory_skew_enabled": + ConfigVar(key="inventory_skew_enabled", + prompt="Would you like to enable inventory skew? (Yes/No) >>> ", + type_str="bool", + default=False, + 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% >>> ", + required_if=lambda: the_money_pit_config_map.get("inventory_skew_enabled").value, + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100), + default=Decimal("50")), + "inventory_range_multiplier": + ConfigVar(key="inventory_range_multiplier", + prompt="What is your tolerable range of inventory around the target, " + "expressed in multiples of your total order size? ", + required_if=lambda: the_money_pit_config_map.get("inventory_skew_enabled").value, + type_str="decimal", + validator=lambda v: validate_decimal(v, min_value=0, inclusive=False), + default=Decimal("1")), + "inventory_price": + ConfigVar(key="inventory_price", + prompt="What is the price of your base asset inventory? ", + type_str="decimal", + validator=lambda v: validate_decimal(v, min_value=Decimal("0"), inclusive=True), + default=Decimal("1"), + ), + "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), + "hanging_orders_enabled": + ConfigVar(key="hanging_orders_enabled", + prompt="Do you want to enable hanging orders? (Yes/No) >>> ", + type_str="bool", + default=False, + validator=validate_bool), + "hanging_orders_cancel_pct": + ConfigVar(key="hanging_orders_cancel_pct", + prompt="At what spread percentage (from mid price) will hanging orders be canceled? " + "(Enter 1 to indicate 1%) >>> ", + required_if=lambda: the_money_pit_config_map.get("hanging_orders_enabled").value, + type_str="decimal", + default=Decimal("10"), + validator=lambda v: validate_decimal(v, 0, 100, inclusive=False)), + "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), + "ask_order_optimization_depth": + ConfigVar(key="ask_order_optimization_depth", + prompt="How deep do you want to go into the order book for calculating " + "the top ask, ignoring dust orders on the top " + "(expressed in base asset amount)? >>> ", + required_if=lambda: the_money_pit_config_map.get("order_optimization_enabled").value, + type_str="decimal", + validator=lambda v: validate_decimal(v, min_value=0), + default=0), + "bid_order_optimization_depth": + ConfigVar(key="bid_order_optimization_depth", + prompt="How deep do you want to go into the order book for calculating " + "the top bid, ignoring dust orders on the top " + "(expressed in base asset amount)? >>> ", + required_if=lambda: the_money_pit_config_map.get("order_optimization_enabled").value, + type_str="decimal", + validator=lambda v: validate_decimal(v, min_value=0), + default=0), + "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: the_money_pit_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: the_money_pit_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: the_money_pit_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: the_money_pit_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: the_money_pit_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"), + "trade_gain_enabled": + ConfigVar(key="trade_gain_enabled", + prompt="Do you want to only allow profitable trades? ((Yes/No) >>> ", + type_str="bool", + validator=validate_bool, + prompt_on_new=True, + default=False), + "trade_gain_hours": + ConfigVar(key="trade_gain_hours", + prompt="How many hours do you want the profit tracking window to be? >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, -1, 10000, True), + default=Decimal(4)), + "trade_gain_trades": + ConfigVar(key="trade_gain_trades", + prompt="How many trades do you want to consider for tracking within the window? >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="int", + validator=lambda v: validate_int(v, min_value=0, inclusive=True), + default=1), + "trade_gain_allowed_loss": + ConfigVar(key="trade_gain_allowed_loss", + prompt="What is your accepted loss? (Enter 20 to indicate 20%) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, -100, 100, True), + default=Decimal(20)), + "trade_gain_profit_wanted": + ConfigVar(key="trade_gain_profit_wanted", + prompt="What is your desired profit? (Enter 20 to indicate 20%) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, -100, 100, True), + default=Decimal(20)), + "trade_gain_ownside_enabled": + ConfigVar(key="trade_gain_ownside_enabled", + prompt="Do you want to include tracking highest sells and lowest buys? (Yes/No) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="bool", + validator=validate_bool, + default=False), + "trade_gain_ownside_allowedloss": + ConfigVar(key="trade_gain_ownside_allowedloss", + prompt="What is your loss margin on high-sells/low-buys? (Enter 20 to indicate 20%) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_ownside_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, -100, 100, True), + default=Decimal(20)), + "trade_gain_careful_enabled": + ConfigVar(key="trade_gain_careful_enabled", + prompt="Enable careful mode and wait for X buys/sells before trading the other side? (Yes/No) >>> ", + type_str="bool", + validator=validate_bool, + default=False), + "trade_gain_careful_limittrades": + ConfigVar(key="trade_gain_careful_limittrades", + prompt="What is your desired trade threshold for careful mode? >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_careful_enabled").value is True, + type_str="int", + validator=lambda v: validate_int(v, min_value=0, inclusive=True), + default=3), + "trade_gain_careful_hours": + ConfigVar(key="trade_gain_careful_hours", + prompt="How many hours do you want the careful window to be? >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_careful_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, -1, 10000, True), + default=Decimal(4)), + "trade_gain_initial_max_buy": + ConfigVar(key="trade_gain_initial_max_buy", + prompt="Enter the initial maximum buy price before any trades have been made " + "(Enter 0 to deactivate this feature) >>> ", + type_str="decimal", + default=Decimal("0"), + validator=lambda v: validate_decimal(v, min_value=0, inclusive=True)), + "trade_gain_initial_min_sell": + ConfigVar(key="trade_gain_initial_min_sell", + prompt="Enter the initial maximum buy price before any trades have been made " + "(Enter 0 to deactivate this feature) >>> ", + type_str="decimal", + default=Decimal("0"), + validator=lambda v: validate_decimal(v, min_value=0, inclusive=True)), + "trade_gain_profit_selloff": + ConfigVar(key="trade_gain_profit_selloff", + prompt="What is your profit percentage at which to sell-off? (Enter 20 to indicate 20%) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, True), + default=Decimal(0)), + "trade_gain_profit_buyin": + ConfigVar(key="trade_gain_profit_buyin", + prompt="What is the percentage below the sell-off at which to buy back in? (Enter 20 to indicate 20%) >>> ", + required_if=lambda: the_money_pit_config_map.get("trade_gain_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, True), + default=Decimal(0)), + "market_indicator_enabled": + ConfigVar(key="market_indicator_enabled", + prompt="Enable Market trend indicator tracking with external API? (Yes/No) >>> ", + type_str="bool", + validator=validate_bool, + default=False), + "market_indicator_url": + ConfigVar(key="market_indicator_url", + prompt="What is the URL of your indicator API? >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="str"), + "market_indicator_apikey": + ConfigVar(key="market_indicator_apikey", + prompt="What is your indicator API key? >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="str"), + "market_indicator_refresh_time": + ConfigVar(key="market_indicator_refresh_time", + prompt="What is your indicator refresh time >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="float", + validator=lambda v: validate_decimal(v, min_value=0, inclusive=False), + default=60), + "market_indicator_reduce_orders_to_pct": + ConfigVar(key="market_indicator_reduce_orders_to_pct", + prompt="What size in percentage would you like to reduce orders to based on the trend?" + "(Enter 0 to stop orders or 1 to indicate 1%) >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="decimal", + validator=lambda v: validate_decimal(v, 0, 100, inclusive=True), + default=Decimal("0")), + "market_indicator_allow_profitable": + ConfigVar(key="market_indicator_allow_profitable", + prompt="Allow profitable trades even with bad signal? (Yes/No) >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="bool", + validator=validate_bool, + default=False), + "market_indicator_disable_expired": + ConfigVar(key="market_indicator_disable_expired", + prompt="Disable trading with expired signal? (Yes/No) >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="bool", + validator=validate_bool, + default=False), + "market_indicator_expiry_minutes": + ConfigVar(key="market_indicator_expiry_minutes", + prompt="How many minutes should the indicator signal expire after? >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="int", + validator=lambda v: validate_int(v, min_value=0, inclusive=True), + default=5), + "market_indicator_use_apitime": + ConfigVar(key="market_indicator_use_apitime", + prompt="Use API supplied indicator time for expiry check? (Yes/No) >>> ", + required_if=lambda: the_money_pit_config_map.get("market_indicator_enabled").value is True, + type_str="bool", + validator=validate_bool, + default=False), +} diff --git a/hummingbot/strategy/the_money_pit/the_money_pit_order_tracker.pxd b/hummingbot/strategy/the_money_pit/the_money_pit_order_tracker.pxd new file mode 100644 index 0000000000..422bc6a2c7 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/the_money_pit_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 TheMoneyPitOrderTracker(OrderTracker): + pass diff --git a/hummingbot/strategy/the_money_pit/the_money_pit_order_tracker.pyx b/hummingbot/strategy/the_money_pit/the_money_pit_order_tracker.pyx new file mode 100644 index 0000000000..f930796854 --- /dev/null +++ b/hummingbot/strategy/the_money_pit/the_money_pit_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 TheMoneyPitOrderTracker(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_the_money_pit_strategy_TEMPLATE.yml b/hummingbot/templates/conf_the_money_pit_strategy_TEMPLATE.yml new file mode 100644 index 0000000000..28e08687e4 --- /dev/null +++ b/hummingbot/templates/conf_the_money_pit_strategy_TEMPLATE.yml @@ -0,0 +1,164 @@ +######################################################## +### The Money Pit strategy config ### +######################################################## + +template_version: 22 +strategy: null + +# Exchange and token parameters. +exchange: null + +# Token trading pair for the exchange, e.g. BTC-USDT +market: null + +# How far away from mid price to place the bid order. +# Spread of 1 = 1% away from mid price at that time. +# Example if mid price is 100 and bid_spread is 1. +# Your bid is placed at 99. +bid_spread: null + +# How far away from mid price to place the ask order. +# Spread of 1 = 1% away from mid price at that time. +# Example if mid price is 100 and ask_spread is 1. +# Your bid is placed at 101. +ask_spread: null + +# Minimum Spread +# How far away from the mid price to cancel active orders +minimum_spread: 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 + +# 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 + +# Price band ceiling. +price_ceiling: null + +# Price band floor. +price_floor: null + +# Whether to alternate between buys and sells (true/false). +ping_pong_enabled: null + +# Whether to enable Inventory skew feature (true/false). +inventory_skew_enabled: null + +# Target base asset inventory percentage target to be maintained (for Inventory skew feature). +inventory_target_base_pct: null + +# The range around the inventory target base percent to maintain, expressed in multiples of total order size (for +# inventory skew feature). +inventory_range_multiplier: null + +# Initial price of the base asset. Note: this setting is not affects anything, the price is kept in the database. +inventory_price: null + +# Number of levels of orders to place on each side of the order book. +order_levels: null + +# Increase or decrease size of consecutive orders after the first order (if order_levels > 1). +order_level_amount: null + +# Order price space between orders (if order_levels > 1). +order_level_spread: null + +# How long to wait before placing the next order in case your order gets filled. +filled_order_delay: null + +# Whether to stop cancellations of orders on the other side (of the order book), +# when one side is filled (hanging orders feature) (true/false). +hanging_orders_enabled: null + +# Spread (from mid price, in percentage) hanging orders will be canceled (Enter 1 to indicate 1%) +hanging_orders_cancel_pct: null + +# Whether to enable order optimization mode (true/false). +order_optimization_enabled: null + +# The depth in base asset amount to be used for finding top ask (for order optimization mode). +ask_order_optimization_depth: null + +# The depth in base asset amount to be used for finding top bid (for order optimization mode). +bid_order_optimization_depth: 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 + + + + +#Trade Tracking +trade_gain_enabled: null +trade_gain_hours: null +trade_gain_trades: null + +trade_gain_allowed_loss: null +trade_gain_profit_wanted: null + +trade_gain_ownside_enabled: null +trade_gain_ownside_allowedloss: null + +trade_gain_careful_enabled: null +trade_gain_careful_limittrades: null +trade_gain_careful_hours: null + +trade_gain_profit_selloff: null +trade_gain_profit_buyin: null + +trade_gain_initial_max_buy: null +trade_gain_initial_min_sell: null + +#Market Indicator Tracking +market_indicator_enabled: null +market_indicator_url: null +market_indicator_apikey: null +market_indicator_refresh_time: null +market_indicator_reduce_orders_to_pct: null +market_indicator_allow_profitable: null +market_indicator_disable_expired: null +market_indicator_expiry_minutes: null +market_indicator_use_apitime: 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 + +# For more detailed information, see: +# https://docs.hummingbot.io/strategies/pure-market-making/#configuration-parameters