Skip to content

Commit

Permalink
Merge pull request #46 from hummingbot/feat/add_grid_strike
Browse files Browse the repository at this point in the history
(feat) update grid strike
  • Loading branch information
cardosofede authored Nov 15, 2024
2 parents 9bcf5f2 + fae1357 commit 08653da
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 20 deletions.
28 changes: 19 additions & 9 deletions bots/controllers/generic/grid_strike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,11 +71,13 @@ 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
self.grid_levels = []

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()
Expand All @@ -90,13 +94,18 @@ 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(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,
Expand Down Expand Up @@ -209,6 +218,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)]
149 changes: 142 additions & 7 deletions bots/scripts/v2_with_controllers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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"]):
Expand Down
29 changes: 29 additions & 0 deletions routers/manage_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
8 changes: 4 additions & 4 deletions services/bots_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 08653da

Please sign in to comment.