From 156714dd57fcbad8d14af105e3a048153a0675c8 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Nov 2024 20:11:32 -0300 Subject: [PATCH 1/6] (feat) update grid strike --- bots/controllers/generic/grid_strike.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/bots/controllers/generic/grid_strike.py b/bots/controllers/generic/grid_strike.py index 902810f..a5e0a4a 100644 --- a/bots/controllers/generic/grid_strike.py +++ b/bots/controllers/generic/grid_strike.py @@ -30,11 +30,13 @@ class GridStrikeConfig(ControllerConfigBase): """ controller_name: str = "grid_strike" candles_config: List[CandlesConfig] = [] + controller_type = "generic" connector_name: str = "binance" trading_pair: str = "BTC-USDT" total_amount_quote: Decimal = Field(default=Decimal("1000"), client_data=ClientFieldData(is_updatable=True)) grid_ranges: List[GridRange] = Field(default=[GridRange(id="R0", start_price=Decimal("40000"), - end_price=Decimal("60000"), total_amount_pct=Decimal("0.1"))], + end_price=Decimal("60000"), + total_amount_pct=Decimal("0.1"))], client_data=ClientFieldData(is_updatable=True)) position_mode: PositionMode = PositionMode.HEDGE leverage: int = 1 @@ -69,11 +71,12 @@ class GridStrike(ControllerBase): def __init__(self, config: GridStrikeConfig, *args, **kwargs): super().__init__(config, *args, **kwargs) self.config = config - self.trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name, - self.config.trading_pair) self._last_grid_levels_update = 0 + self.trading_rules = None def _calculate_grid_config(self): + self.trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name, + self.config.trading_pair) grid_levels = [] if self.config.min_spread_between_orders: spread_between_orders = self.config.min_spread_between_orders * self.get_mid_price() @@ -92,11 +95,14 @@ def _calculate_grid_config(self): step = (grid_range.end_price - grid_range.start_price) / grid_range.end_price / orders amount_quote = total_amount / orders for i, price in enumerate(prices): - price_quantized = self.market_data_provider.quantize_order_price(self.config.connector_name, - self.config.trading_pair, price) - # amount_quantized = self.market_data_provider.quantize_order_amount(self.config.connector_name, - # self.config.trading_pair, amount_quote / self.get_mid_price()) - amount_quantized = amount_quote / self.get_mid_price() + price_quantized = self.market_data_provider.quantize_order_price( + self.config.connector_name, + self.config.trading_pair, price) + amount_quantized = self.market_data_provider.quantize_order_amount( + self.config.connector_name, + self.config.trading_pair, + amount_quote / self.get_mid_price()) + # amount_quantized = amount_quote / self.get_mid_price() grid_levels.append(GridLevel(id=f"{grid_range.id}_P{i}", price=price_quantized, amount=amount_quantized, @@ -209,6 +215,7 @@ def determine_stop_executor_actions(self) -> List[ExecutorAction]: short_executors_to_stop = [executor.id for executor in active_executors_order_placed if executor.side == TradeType.SELL and executor.config.entry_price >= short_activation_bounds] - executors_id_to_stop = set(active_executor_of_non_active_ranges + long_executors_to_stop + short_executors_to_stop) + executors_id_to_stop = set( + active_executor_of_non_active_ranges + long_executors_to_stop + short_executors_to_stop) return [StopExecutorAction(controller_id=self.config.id, executor_id=executor) for executor in list(executors_id_to_stop)] From 09173ebc7d4ed1b0007008d3f3477bd2e3d0ed8d Mon Sep 17 00:00:00 2001 From: cardosofede Date: Wed, 13 Nov 2024 11:47:13 -0300 Subject: [PATCH 2/6] (feat) fix typo --- services/bots_orchestrator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/bots_orchestrator.py b/services/bots_orchestrator.py index 24ae656..2fe5ba7 100644 --- a/services/bots_orchestrator.py +++ b/services/bots_orchestrator.py @@ -154,11 +154,11 @@ def get_all_bots_status(self): def get_bot_status(self, bot_name): if bot_name in self.active_bots: try: - broker_listner = self.active_bots[bot_name]["broker_listener"] - controllers_performance = broker_listner.get_bot_performance() + broker_listener = self.active_bots[bot_name]["broker_listener"] + controllers_performance = broker_listener.get_bot_performance() performance = self.determine_controller_performance(controllers_performance) - error_logs = broker_listner.get_bot_error_logs() - general_logs = broker_listner.get_bot_general_logs() + error_logs = broker_listener.get_bot_error_logs() + general_logs = broker_listener.get_bot_general_logs() status = "running" if len(performance) > 0 else "stopped" return { "status": status, From 58144306b0a86b24e700853495f317eca0fbfadf Mon Sep 17 00:00:00 2001 From: cardosofede Date: Thu, 14 Nov 2024 18:42:30 -0300 Subject: [PATCH 3/6] (feat) update v2 with controllers --- bots/scripts/v2_with_controllers.py | 149 ++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 7 deletions(-) diff --git a/bots/scripts/v2_with_controllers.py b/bots/scripts/v2_with_controllers.py index a2c1c8a..7a2e9a1 100644 --- a/bots/scripts/v2_with_controllers.py +++ b/bots/scripts/v2_with_controllers.py @@ -1,10 +1,12 @@ import os import time +from decimal import Decimal from typing import Dict, List, Optional, Set from hummingbot.client.hummingbot_application import HummingbotApplication from hummingbot.connector.connector_base import ConnectorBase from hummingbot.core.clock import Clock +from hummingbot.core.data_type.common import OrderType, TradeType from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.remote_iface.mqtt import ETopicPublisher from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase @@ -18,6 +20,13 @@ class GenericV2StrategyWithCashOutConfig(StrategyV2ConfigBase): candles_config: List[CandlesConfig] = [] markets: Dict[str, Set[str]] = {} time_to_cash_out: Optional[int] = None + max_global_drawdown: Optional[float] = None + max_controller_drawdown: Optional[float] = None + performance_report_interval: int = 1 + rebalance_interval: Optional[int] = None + extra_inventory: Optional[float] = 0.02 + min_amount_to_rebalance_usd: Decimal = Decimal("8") + asset_to_rebalance: str = "USDT" class GenericV2StrategyWithCashOut(StrategyV2Base): @@ -36,9 +45,15 @@ def __init__(self, connectors: Dict[str, ConnectorBase], config: GenericV2Strate super().__init__(connectors, config) self.config = config self.cashing_out = False + self.max_pnl_by_controller = {} + self.performance_reports = {} + self.max_global_pnl = Decimal("0") + self.drawdown_exited_controllers = [] self.closed_executors_buffer: int = 30 - self.performance_report_interval: int = 1 + self.performance_report_interval: int = self.config.performance_report_interval + self.rebalance_interval: int = self.config.rebalance_interval self._last_performance_report_timestamp = 0 + self._last_rebalance_check_timestamp = 0 hb_app = HummingbotApplication.main_application() self.mqtt_enabled = hb_app._mqtt is not None self._pub: Optional[ETopicPublisher] = None @@ -58,22 +73,139 @@ def start(self, clock: Clock, timestamp: float) -> None: if self.mqtt_enabled: self._pub = ETopicPublisher("performance", use_bot_prefix=True) - def on_stop(self): + async def on_stop(self): + await super().on_stop() if self.mqtt_enabled: self._pub({controller_id: {} for controller_id in self.controllers.keys()}) self._pub = None def on_tick(self): super().on_tick() + self.performance_reports = {controller_id: self.executor_orchestrator.generate_performance_report( + controller_id=controller_id).dict() for controller_id in self.controllers.keys()} + self.control_rebalance() self.control_cash_out() + self.control_max_drawdown() self.send_performance_report() + def control_rebalance(self): + if self.rebalance_interval and self._last_rebalance_check_timestamp + self.rebalance_interval <= self.current_timestamp: + balance_required = {} + for controller_id, controller in self.controllers.items(): + connector_name = controller.config.dict().get("connector_name") + if connector_name and "perpetual" in connector_name: + continue + if connector_name not in balance_required: + balance_required[connector_name] = {} + tokens_required = controller.get_balance_requirements() + for token, amount in tokens_required: + if token not in balance_required[connector_name]: + balance_required[connector_name][token] = amount + else: + balance_required[connector_name][token] += amount + for connector_name, balance_requirements in balance_required.items(): + connector = self.connectors[connector_name] + for token, amount in balance_requirements.items(): + if token == self.config.asset_to_rebalance: + continue + balance = connector.get_balance(token) + trading_pair = f"{token}-{self.config.asset_to_rebalance}" + mid_price = connector.get_mid_price(trading_pair) + trading_rule = connector.trading_rules[trading_pair] + amount_with_safe_margin = amount * (1 + Decimal(self.config.extra_inventory)) + active_executors_for_pair = self.filter_executors( + executors=self.get_all_executors(), + filter_func=lambda x: x.is_active and x.trading_pair == trading_pair and + x.connector_name == connector_name + ) + unmatched_amount = sum([executor.filled_amount_quote for executor in active_executors_for_pair if + executor.side == TradeType.SELL]) - sum( + [executor.filled_amount_quote for executor in active_executors_for_pair if + executor.side == TradeType.BUY]) + balance += unmatched_amount / mid_price + base_balance_diff = balance - amount_with_safe_margin + abs_balance_diff = abs(base_balance_diff) + trading_rules_condition = abs_balance_diff > trading_rule.min_order_size and \ + abs_balance_diff * mid_price > trading_rule.min_notional_size and \ + abs_balance_diff * mid_price > self.config.min_amount_to_rebalance_usd + order_type = OrderType.MARKET + if base_balance_diff > 0: + if trading_rules_condition: + self.logger().info( + f"Rebalance: Selling {amount_with_safe_margin} {token} to " + f"{self.config.asset_to_rebalance}. Balance: {balance} | " + f"Executors unmatched balance {unmatched_amount / mid_price}") + connector.sell( + trading_pair=trading_pair, + amount=abs_balance_diff, + order_type=order_type, + price=mid_price) + else: + self.logger().info( + "Skipping rebalance due a low amount to sell that may cause future imbalance") + else: + if not trading_rules_condition: + amount = max( + [self.config.min_amount_to_rebalance_usd / mid_price, trading_rule.min_order_size, + trading_rule.min_notional_size / mid_price]) + self.logger().info( + f"Rebalance: Buying for a higher value to avoid future imbalance {amount} {token} to " + f"{self.config.asset_to_rebalance}. Balance: {balance} | " + f"Executors unmatched balance {unmatched_amount}") + else: + amount = abs_balance_diff + self.logger().info( + f"Rebalance: Buying {amount} {token} to {self.config.asset_to_rebalance}. " + f"Balance: {balance} | Executors unmatched balance {unmatched_amount}") + connector.buy( + trading_pair=trading_pair, + amount=amount, + order_type=order_type, + price=mid_price) + self._last_rebalance_check_timestamp = self.current_timestamp + + def control_max_drawdown(self): + if self.config.max_controller_drawdown: + self.check_max_controller_drawdown() + if self.config.max_global_drawdown: + self.check_max_global_drawdown() + + def check_max_controller_drawdown(self): + for controller_id, controller in self.controllers.items(): + controller_pnl = self.performance_reports[controller_id]["global_pnl_quote"] + last_max_pnl = self.max_pnl_by_controller[controller_id] + if controller_pnl > last_max_pnl: + self.max_pnl_by_controller[controller_id] = controller_pnl + else: + current_drawdown = last_max_pnl - controller_pnl + if current_drawdown > self.config.max_controller_drawdown: + self.logger().info(f"Controller {controller_id} reached max drawdown. Stopping the controller.") + controller.stop() + executors_order_placed = self.filter_executors( + executors=self.executors_info[controller_id], + filter_func=lambda x: x.is_active and not x.is_trading, + ) + self.executor_orchestrator.execute_actions( + actions=[StopExecutorAction(controller_id=controller_id, executor_id=executor.id) for executor + in executors_order_placed] + ) + self.drawdown_exited_controllers.append(controller_id) + + def check_max_global_drawdown(self): + current_global_pnl = sum([report["global_pnl_quote"] for report in self.performance_reports.values()]) + if current_global_pnl > self.max_global_pnl: + self.max_global_pnl = current_global_pnl + else: + current_global_drawdown = self.max_global_pnl - current_global_pnl + if current_global_drawdown > self.config.max_global_drawdown: + self.drawdown_exited_controllers.extend(list(self.controllers.keys())) + self.logger().info("Global drawdown reached. Stopping the strategy.") + HummingbotApplication.main_application().stop() + def send_performance_report(self): - if self.current_timestamp - self._last_performance_report_timestamp >= self.performance_report_interval \ - and self.mqtt_enabled: - performance_reports = {controller_id: self.executor_orchestrator.generate_performance_report( - controller_id=controller_id).dict() for controller_id in self.controllers.keys()} - self._pub(performance_reports) + if self.current_timestamp - self._last_performance_report_timestamp >= self.performance_report_interval and \ + self.mqtt_enabled: + self._pub(self.performance_reports) self._last_performance_report_timestamp = self.current_timestamp def control_cash_out(self): @@ -102,6 +234,8 @@ def check_manual_cash_out(self): [StopExecutorAction(executor_id=executor.id, controller_id=executor.controller_id) for executor in executors_to_stop]) if not controller.config.manual_kill_switch and controller.status == RunnableStatus.TERMINATED: + if controller_id in self.drawdown_exited_controllers: + continue self.logger().info(f"Restarting controller {controller_id}.") controller.start() @@ -131,6 +265,7 @@ def stop_actions_proposal(self) -> List[StopExecutorAction]: def apply_initial_setting(self): connectors_position_mode = {} for controller_id, controller in self.controllers.items(): + self.max_pnl_by_controller[controller_id] = Decimal("0") config_dict = controller.config.dict() if "connector_name" in config_dict: if self.is_perpetual(config_dict["connector_name"]): From 4963bd2c3b844575e00284860c289ac291ae2a95 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Thu, 14 Nov 2024 18:42:51 -0300 Subject: [PATCH 4/6] (feat) add filter for 0 orders --- bots/controllers/generic/grid_strike.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bots/controllers/generic/grid_strike.py b/bots/controllers/generic/grid_strike.py index a5e0a4a..f506250 100644 --- a/bots/controllers/generic/grid_strike.py +++ b/bots/controllers/generic/grid_strike.py @@ -73,6 +73,7 @@ def __init__(self, config: GridStrikeConfig, *args, **kwargs): self.config = config self._last_grid_levels_update = 0 self.trading_rules = None + self.grid_levels = [] def _calculate_grid_config(self): self.trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name, @@ -93,6 +94,9 @@ def _calculate_grid_config(self): orders = int(min(theoretical_orders_by_step, theoretical_orders_by_amount)) prices = Distributions.linear(orders, float(grid_range.start_price), float(grid_range.end_price)) step = (grid_range.end_price - grid_range.start_price) / grid_range.end_price / orders + if orders == 0: + self.logger().warning(f"Grid range {grid_range.id} has no orders, change the parameters " + f"(min order amount, amount pct, min spread between orders or total amount)") amount_quote = total_amount / orders for i, price in enumerate(prices): price_quantized = self.market_data_provider.quantize_order_price( @@ -100,8 +104,7 @@ def _calculate_grid_config(self): self.config.trading_pair, price) amount_quantized = self.market_data_provider.quantize_order_amount( self.config.connector_name, - self.config.trading_pair, - amount_quote / self.get_mid_price()) + self.config.trading_pair, amount_quote / self.get_mid_price()) # amount_quantized = amount_quote / self.get_mid_price() grid_levels.append(GridLevel(id=f"{grid_range.id}_P{i}", price=price_quantized, From e62b6277887c01e12d8b9625efe9d39d9c78ba5d Mon Sep 17 00:00:00 2001 From: cardosofede Date: Thu, 14 Nov 2024 18:55:14 -0300 Subject: [PATCH 5/6] (feat) add routes to delete controllers and scripts configs --- routers/manage_files.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/routers/manage_files.py b/routers/manage_files.py index 127fbfd..129756c 100644 --- a/routers/manage_files.py +++ b/routers/manage_files.py @@ -159,3 +159,32 @@ async def delete_controller_config(config_name: str): return {"message": f"Controller configuration {config_name} deleted successfully."} except FileNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) + + +@router.post("delete-script-config", status_code=status.HTTP_200_OK) +async def delete_script_config(config_name: str): + try: + file_system.delete_file('conf/scripts', config_name) + return {"message": f"Script configuration {config_name} deleted successfully."} + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.post("delete-all-controller-configs", status_code=status.HTTP_200_OK) +async def delete_all_controller_configs(): + try: + for file in file_system.list_files('conf/controllers'): + file_system.delete_file('conf/controllers', file) + return {"message": "All controller configurations deleted successfully."} + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.post("delete-all-script-configs", status_code=status.HTTP_200_OK) +async def delete_all_script_configs(): + try: + for file in file_system.list_files('conf/scripts'): + file_system.delete_file('conf/scripts', file) + return {"message": "All script configurations deleted successfully."} + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) From fae1357eb8e1c72f09a653eb2de8326a5485e419 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Thu, 14 Nov 2024 19:30:14 -0300 Subject: [PATCH 6/6] (feat) add path --- routers/manage_files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routers/manage_files.py b/routers/manage_files.py index 129756c..4d3fd39 100644 --- a/routers/manage_files.py +++ b/routers/manage_files.py @@ -161,7 +161,7 @@ async def delete_controller_config(config_name: str): raise HTTPException(status_code=404, detail=str(e)) -@router.post("delete-script-config", status_code=status.HTTP_200_OK) +@router.post("/delete-script-config", status_code=status.HTTP_200_OK) async def delete_script_config(config_name: str): try: file_system.delete_file('conf/scripts', config_name) @@ -170,7 +170,7 @@ async def delete_script_config(config_name: str): raise HTTPException(status_code=404, detail=str(e)) -@router.post("delete-all-controller-configs", status_code=status.HTTP_200_OK) +@router.post("/delete-all-controller-configs", status_code=status.HTTP_200_OK) async def delete_all_controller_configs(): try: for file in file_system.list_files('conf/controllers'): @@ -180,7 +180,7 @@ async def delete_all_controller_configs(): raise HTTPException(status_code=404, detail=str(e)) -@router.post("delete-all-script-configs", status_code=status.HTTP_200_OK) +@router.post("/delete-all-script-configs", status_code=status.HTTP_200_OK) async def delete_all_script_configs(): try: for file in file_system.list_files('conf/scripts'):