Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) update grid strike #46

Merged
merged 6 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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