from decimal import Decimal from typing import List, Optional import pandas_ta as ta # noqa: F401 from pydantic import Field, validator from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.core.data_type.common import TradeType from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.market_making_controller_base import ( MarketMakingControllerBase, MarketMakingControllerConfigBase, ) from hummingbot.strategy_v2.executors.dca_executor.data_types import DCAExecutorConfig, DCAMode from hummingbot.strategy_v2.models.executor_actions import ExecutorAction, StopExecutorAction class DManMakerV2Config(MarketMakingControllerConfigBase): """ Configuration required to run the D-Man Maker V2 strategy. """ controller_name: str = "dman_maker_v2" candles_config: List[CandlesConfig] = [] # DCA configuration dca_spreads: List[Decimal] = Field( default="0.01,0.02,0.04,0.08", client_data=ClientFieldData( prompt_on_new=True, prompt=lambda mi: "Enter a comma-separated list of spreads for each DCA level: ")) dca_amounts: List[Decimal] = Field( default="0.1,0.2,0.4,0.8", client_data=ClientFieldData( prompt_on_new=True, prompt=lambda mi: "Enter a comma-separated list of amounts for each DCA level: ")) time_limit: int = Field( default=60 * 60 * 24 * 7, gt=0, client_data=ClientFieldData( prompt=lambda mi: "Enter the time limit for each DCA level: ", prompt_on_new=False)) stop_loss: Decimal = Field( default=Decimal("0.03"), gt=0, client_data=ClientFieldData( prompt=lambda mi: "Enter the stop loss (as a decimal, e.g., 0.03 for 3%): ", prompt_on_new=True)) top_executor_refresh_time: Optional[float] = Field( default=None, client_data=ClientFieldData( is_updatable=True, prompt_on_new=False)) executor_activation_bounds: Optional[List[Decimal]] = Field( default=None, client_data=ClientFieldData( is_updatable=True, prompt=lambda mi: "Enter the activation bounds for the orders " "(e.g., 0.01 activates the next order when the price is closer than 1%): ", prompt_on_new=False)) @validator("executor_activation_bounds", pre=True, always=True) def parse_activation_bounds(cls, v): if isinstance(v, list): return [Decimal(val) for val in v] elif isinstance(v, str): if v == "": return None return [Decimal(val) for val in v.split(",")] return v @validator('dca_spreads', pre=True, always=True) def parse_spreads(cls, v): if v is None: return [] if isinstance(v, str): if v == "": return [] return [float(x.strip()) for x in v.split(',')] return v @validator('dca_amounts', pre=True, always=True) def parse_and_validate_amounts(cls, v, values, field): if v is None or v == "": return [1 for _ in values[values['dca_spreads']]] if isinstance(v, str): return [float(x.strip()) for x in v.split(',')] elif isinstance(v, list) and len(v) != len(values['dca_spreads']): raise ValueError( f"The number of {field.name} must match the number of {values['dca_spreads']}.") return v class DManMakerV2(MarketMakingControllerBase): def __init__(self, config: DManMakerV2Config, *args, **kwargs): super().__init__(config, *args, **kwargs) self.config = config self.dca_amounts_pct = [Decimal(amount) / sum(self.config.dca_amounts) for amount in self.config.dca_amounts] self.spreads = self.config.dca_spreads def first_level_refresh_condition(self, executor): if self.config.top_executor_refresh_time is not None: if self.get_level_from_level_id(executor.custom_info["level_id"]) == 0: return self.market_data_provider.time() - executor.timestamp > self.config.top_executor_refresh_time * 1000 return False def order_level_refresh_condition(self, executor): return self.market_data_provider.time() - executor.timestamp > self.config.executor_refresh_time * 1000 def executors_to_refresh(self) -> List[ExecutorAction]: executors_to_refresh = self.filter_executors( executors=self.executors_info, filter_func=lambda x: not x.is_trading and x.is_active and (self.order_level_refresh_condition(x) or self.first_level_refresh_condition(x))) return [StopExecutorAction( controller_id=self.config.id, executor_id=executor.id) for executor in executors_to_refresh] def get_executor_config(self, level_id: str, price: Decimal, amount: Decimal): trade_type = self.get_trade_type_from_level_id(level_id) if trade_type == TradeType.BUY: prices = [price * (1 - spread) for spread in self.spreads] else: prices = [price * (1 + spread) for spread in self.spreads] amounts = [amount * pct for pct in self.dca_amounts_pct] amounts_quote = [amount * price for amount, price in zip(amounts, prices)] return DCAExecutorConfig( timestamp=self.market_data_provider.time(), connector_name=self.config.connector_name, trading_pair=self.config.trading_pair, mode=DCAMode.MAKER, side=trade_type, prices=prices, amounts_quote=amounts_quote, level_id=level_id, time_limit=self.config.time_limit, stop_loss=self.config.stop_loss, take_profit=self.config.take_profit, trailing_stop=self.config.trailing_stop, activation_bounds=self.config.executor_activation_bounds, leverage=self.config.leverage, )