From 97c450e778de819c97ce46abdce4d45ff182aa6f Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 11 Apr 2025 16:01:45 -0300 Subject: [PATCH] (feat) update controllers --- .../directional_trading/bollinger_v1.py | 62 ++-- .../directional_trading/dman_v3.py | 142 ++++---- .../directional_trading/macd_bb_v1.py | 76 ++-- .../directional_trading/supertrend_v1.py | 48 ++- bots/controllers/generic/grid_strike.py | 340 ++++++++---------- .../generic/spot_perp_arbitrage.py | 196 ---------- .../generic/xemm_multiple_levels.py | 79 ++-- .../market_making/dman_maker_v2.py | 58 +-- bots/controllers/market_making/pmm_dynamic.py | 79 ++-- bots/controllers/market_making/pmm_simple.py | 5 +- 10 files changed, 396 insertions(+), 689 deletions(-) delete mode 100644 bots/controllers/generic/spot_perp_arbitrage.py diff --git a/bots/controllers/directional_trading/bollinger_v1.py b/bots/controllers/directional_trading/bollinger_v1.py index 6a075ad..bfb476b 100644 --- a/bots/controllers/directional_trading/bollinger_v1.py +++ b/bots/controllers/directional_trading/bollinger_v1.py @@ -1,9 +1,9 @@ from typing import List import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( DirectionalTradingControllerBase, @@ -12,56 +12,42 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo class BollingerV1ControllerConfig(DirectionalTradingControllerConfigBase): - controller_name = "bollinger_v1" + controller_name: str = "bollinger_v1" candles_config: List[CandlesConfig] = [] candles_connector: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", + "prompt_on_new": True}) candles_trading_pair: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", + "prompt_on_new": True}) interval: str = Field( default="3m", - client_data=ClientFieldData( - prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", - prompt_on_new=False)) + json_schema_extra={ + "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", + "prompt_on_new": True}) bb_length: int = Field( default=100, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands length: ", - prompt_on_new=True)) - bb_std: float = Field( - default=2.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands standard deviation: ", - prompt_on_new=False)) - bb_long_threshold: float = Field( - default=0.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands long threshold: ", - prompt_on_new=True)) - bb_short_threshold: float = Field( - default=1.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands short threshold: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True}) + bb_std: float = Field(default=2.0) + bb_long_threshold: float = Field(default=0.0) + bb_short_threshold: float = Field(default=1.0) - @validator("candles_connector", pre=True, always=True) - def set_candles_connector(cls, v, values): + @field_validator("candles_connector", mode="before") + @classmethod + def set_candles_connector(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("connector_name") + return validation_info.data.get("connector_name") return v - @validator("candles_trading_pair", pre=True, always=True) - def set_candles_trading_pair(cls, v, values): + @field_validator("candles_trading_pair", mode="before") + @classmethod + def set_candles_trading_pair(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("trading_pair") + return validation_info.data.get("trading_pair") return v diff --git a/bots/controllers/directional_trading/dman_v3.py b/bots/controllers/directional_trading/dman_v3.py index 646b4ec..8e4ee07 100644 --- a/bots/controllers/directional_trading/dman_v3.py +++ b/bots/controllers/directional_trading/dman_v3.py @@ -3,9 +3,9 @@ from decimal import Decimal from typing import List, Optional, Tuple import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo -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.directional_trading_controller_base import ( @@ -21,75 +21,63 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase): candles_config: List[CandlesConfig] = [] candles_connector: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",) - ) + json_schema_extra={ + "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", + "prompt_on_new": True}) candles_trading_pair: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ",) - ) + json_schema_extra={ + "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", + "prompt_on_new": True}) interval: str = Field( - default="30m", - client_data=ClientFieldData( - prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", - prompt_on_new=True)) + default="3m", + json_schema_extra={ + "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", + "prompt_on_new": True}) bb_length: int = Field( default=100, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands length: ", - prompt_on_new=True)) - bb_std: float = Field( - default=2.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands standard deviation: ", - prompt_on_new=False)) - bb_long_threshold: float = Field( - default=0.0, - client_data=ClientFieldData( - is_updatable=True, - prompt=lambda mi: "Enter the Bollinger Bands long threshold: ", - prompt_on_new=True)) - bb_short_threshold: float = Field( - default=1.0, - client_data=ClientFieldData( - is_updatable=True, - prompt=lambda mi: "Enter the Bollinger Bands short threshold: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True}) + bb_std: float = Field(default=2.0) + bb_long_threshold: float = Field(default=0.0) + bb_short_threshold: float = Field(default=1.0) + trailing_stop: Optional[TrailingStop] = Field( + default="0.015,0.005", + json_schema_extra={ + "prompt": "Enter the trailing stop parameters (activation_price, trailing_delta) as a comma-separated list: ", + "prompt_on_new": True, + } + ) dca_spreads: List[Decimal] = Field( default="0.001,0.018,0.15,0.25", - client_data=ClientFieldData( - prompt=lambda mi: "Enter the spreads for each DCA level (comma-separated) if dynamic_spread=True this value " - "will multiply the Bollinger Bands width, e.g. if the Bollinger Bands width is 0.1 (10%)" - "and the spread is 0.2, the distance of the order to the current price will be 0.02 (2%) ", - prompt_on_new=True)) + json_schema_extra={ + "prompt": "Enter the spreads for each DCA level (comma-separated) if dynamic_spread=True this value " + "will multiply the Bollinger Bands width, e.g. if the Bollinger Bands width is 0.1 (10%)" + "and the spread is 0.2, the distance of the order to the current price will be 0.02 (2%) ", + "prompt_on_new": True}, + ) dca_amounts_pct: List[Decimal] = Field( default=None, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the amounts for each DCA level (as a percentage of the total balance, " - "comma-separated). Don't worry about the final sum, it will be normalized. ", - prompt_on_new=True)) + json_schema_extra={ + "prompt": "Enter the amounts for each DCA level (as a percentage of the total balance, " + "comma-separated). Don't worry about the final sum, it will be normalized. ", + "prompt_on_new": True}, + ) dynamic_order_spread: bool = Field( default=None, - client_data=ClientFieldData( - prompt=lambda mi: "Do you want to make the spread dynamic? (Yes/No) ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Do you want to make the spread dynamic? (Yes/No) ", "prompt_on_new": True}) dynamic_target: bool = Field( default=None, - client_data=ClientFieldData( - prompt=lambda mi: "Do you want to make the target dynamic? (Yes/No) ", - prompt_on_new=True)) - + json_schema_extra={"prompt": "Do you want to make the target dynamic? (Yes/No) ", "prompt_on_new": True}) activation_bounds: Optional[List[Decimal]] = Field( default=None, - client_data=ClientFieldData( - 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=True)) + json_schema_extra={ + "prompt": "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": True, + } + ) - @validator("activation_bounds", pre=True, always=True) + @field_validator("activation_bounds", mode="before") + @classmethod def parse_activation_bounds(cls, v): if isinstance(v, str): if v == "": @@ -99,15 +87,17 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase): return [Decimal(val) for val in v] return v - @validator('dca_spreads', pre=True, always=True) + @field_validator('dca_spreads', mode="before") + @classmethod def validate_spreads(cls, v): if isinstance(v, str): return [Decimal(val) for val in v.split(",")] return v - @validator('dca_amounts_pct', pre=True, always=True) - def validate_amounts(cls, v, values): - spreads = values.get("dca_spreads") + @field_validator('dca_amounts_pct', mode="before") + @classmethod + def validate_amounts(cls, v, validation_info: ValidationInfo): + spreads = validation_info.data.get("dca_spreads") if isinstance(v, str): if v == "": return [Decimal('1.0') / len(spreads) for _ in spreads] @@ -119,6 +109,20 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase): return [Decimal('1.0') / len(spreads) for _ in spreads] return v + @field_validator("candles_connector", mode="before") + @classmethod + def set_candles_connector(cls, v, validation_info: ValidationInfo): + if v is None or v == "": + return validation_info.data.get("connector_name") + return v + + @field_validator("candles_trading_pair", mode="before") + @classmethod + def set_candles_trading_pair(cls, v, validation_info: ValidationInfo): + if v is None or v == "": + return validation_info.data.get("trading_pair") + return v + def get_spreads_and_amounts_in_quote(self, trade_type: TradeType, total_amount_quote: Decimal) -> Tuple[List[Decimal], List[Decimal]]: amounts_pct = self.dca_amounts_pct if amounts_pct is None: @@ -133,18 +137,6 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase): return self.dca_spreads, [amt_pct * total_amount_quote for amt_pct in normalized_amounts_pct] - @validator("candles_connector", pre=True, always=True) - def set_candles_connector(cls, v, values): - if v is None or v == "": - return values.get("connector_name") - return v - - @validator("candles_trading_pair", pre=True, always=True) - def set_candles_trading_pair(cls, v, values): - if v is None or v == "": - return values.get("trading_pair") - return v - class DManV3Controller(DirectionalTradingControllerBase): """ @@ -201,8 +193,12 @@ class DManV3Controller(DirectionalTradingControllerBase): prices = [price * (1 + spread * spread_multiplier) for spread in spread] if self.config.dynamic_target: stop_loss = self.config.stop_loss * spread_multiplier - trailing_stop = TrailingStop(activation_price=self.config.trailing_stop.activation_price * spread_multiplier, - trailing_delta=self.config.trailing_stop.trailing_delta * spread_multiplier) + if self.config.trailing_stop: + trailing_stop = TrailingStop( + activation_price=self.config.trailing_stop.activation_price * spread_multiplier, + trailing_delta=self.config.trailing_stop.trailing_delta * spread_multiplier) + else: + trailing_stop = None else: stop_loss = self.config.stop_loss trailing_stop = self.config.trailing_stop diff --git a/bots/controllers/directional_trading/macd_bb_v1.py b/bots/controllers/directional_trading/macd_bb_v1.py index a516472..f792ecf 100644 --- a/bots/controllers/directional_trading/macd_bb_v1.py +++ b/bots/controllers/directional_trading/macd_bb_v1.py @@ -1,9 +1,9 @@ from typing import List import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( DirectionalTradingControllerBase, @@ -12,71 +12,51 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo class MACDBBV1ControllerConfig(DirectionalTradingControllerConfigBase): - controller_name = "macd_bb_v1" + controller_name: str = "macd_bb_v1" candles_config: List[CandlesConfig] = [] candles_connector: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", + "prompt_on_new": True}) candles_trading_pair: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", + "prompt_on_new": True}) interval: str = Field( default="3m", - client_data=ClientFieldData( - prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", - prompt_on_new=False)) + json_schema_extra={ + "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", + "prompt_on_new": True}) bb_length: int = Field( default=100, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands length: ", - prompt_on_new=True)) - bb_std: float = Field( - default=2.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands standard deviation: ", - prompt_on_new=False)) - bb_long_threshold: float = Field( - default=0.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands long threshold: ", - prompt_on_new=True)) - bb_short_threshold: float = Field( - default=1.0, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the Bollinger Bands short threshold: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True}) + bb_std: float = Field(default=2.0) + bb_long_threshold: float = Field(default=0.0) + bb_short_threshold: float = Field(default=1.0) macd_fast: int = Field( default=21, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD fast period: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the MACD fast period: ", "prompt_on_new": True}) macd_slow: int = Field( default=42, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD slow period: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the MACD slow period: ", "prompt_on_new": True}) macd_signal: int = Field( default=9, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD signal period: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the MACD signal period: ", "prompt_on_new": True}) - @validator("candles_connector", pre=True, always=True) - def set_candles_connector(cls, v, values): + @field_validator("candles_connector", mode="before") + @classmethod + def set_candles_connector(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("connector_name") + return validation_info.data.get("connector_name") return v - @validator("candles_trading_pair", pre=True, always=True) - def set_candles_trading_pair(cls, v, values): + @field_validator("candles_trading_pair", mode="before") + @classmethod + def set_candles_trading_pair(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("trading_pair") + return validation_info.data.get("trading_pair") return v @@ -84,7 +64,7 @@ class MACDBBV1Controller(DirectionalTradingControllerBase): def __init__(self, config: MACDBBV1ControllerConfig, *args, **kwargs): self.config = config - self.max_records = max(config.macd_slow, config.macd_fast, config.macd_signal, config.bb_length) + 200 + self.max_records = max(config.macd_slow, config.macd_fast, config.macd_signal, config.bb_length) + 20 if len(self.config.candles_config) == 0: self.config.candles_config = [CandlesConfig( connector=config.candles_connector, diff --git a/bots/controllers/directional_trading/supertrend_v1.py b/bots/controllers/directional_trading/supertrend_v1.py index 6b634bf..10f3ea8 100644 --- a/bots/controllers/directional_trading/supertrend_v1.py +++ b/bots/controllers/directional_trading/supertrend_v1.py @@ -1,9 +1,9 @@ -from typing import List, Optional +from typing import List import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( DirectionalTradingControllerBase, @@ -14,23 +14,41 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo class SuperTrendConfig(DirectionalTradingControllerConfigBase): controller_name: str = "supertrend_v1" candles_config: List[CandlesConfig] = [] - candles_connector: Optional[str] = Field(default=None, client_data=ClientFieldData(prompt_on_new=True, prompt=lambda mi: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", )) - candles_trading_pair: Optional[str] = Field(default=None, client_data=ClientFieldData(prompt_on_new=True, prompt=lambda mi: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", )) - interval: str = Field(default="3m", client_data=ClientFieldData(prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", prompt_on_new=False)) - length: int = Field(default=20, client_data=ClientFieldData(prompt=lambda mi: "Enter the supertrend length: ", prompt_on_new=True)) - multiplier: float = Field(default=4.0, client_data=ClientFieldData(prompt=lambda mi: "Enter the supertrend multiplier: ", prompt_on_new=True)) - percentage_threshold: float = Field(default=0.01, client_data=ClientFieldData(prompt=lambda mi: "Enter the percentage threshold: ", prompt_on_new=True)) + candles_connector: str = Field( + default=None, + json_schema_extra={ + "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", + "prompt_on_new": True}) + candles_trading_pair: str = Field( + default=None, + json_schema_extra={ + "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", + "prompt_on_new": True}) + interval: str = Field( + default="3m", + json_schema_extra={"prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", "prompt_on_new": True}) + length: int = Field( + default=20, + json_schema_extra={"prompt": "Enter the supertrend length: ", "prompt_on_new": True}) + multiplier: float = Field( + default=4.0, + json_schema_extra={"prompt": "Enter the supertrend multiplier: ", "prompt_on_new": True}) + percentage_threshold: float = Field( + default=0.01, + json_schema_extra={"prompt": "Enter the percentage threshold: ", "prompt_on_new": True}) - @validator("candles_connector", pre=True, always=True) - def set_candles_connector(cls, v, values): + @field_validator("candles_connector", mode="before") + @classmethod + def set_candles_connector(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("connector_name") + return validation_info.data.get("connector_name") return v - @validator("candles_trading_pair", pre=True, always=True) - def set_candles_trading_pair(cls, v, values): + @field_validator("candles_trading_pair", mode="before") + @classmethod + def set_candles_trading_pair(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("trading_pair") + return validation_info.data.get("trading_pair") return v diff --git a/bots/controllers/generic/grid_strike.py b/bots/controllers/generic/grid_strike.py index d4e7542..20daf9e 100644 --- a/bots/controllers/generic/grid_strike.py +++ b/bots/controllers/generic/grid_strike.py @@ -1,55 +1,56 @@ from decimal import Decimal from typing import Dict, List, Optional, Set -from pydantic import BaseModel, Field +from pydantic import Field -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.core.data_type.common import OrderType, PositionMode, PriceType, TradeType -from hummingbot.core.data_type.trade_fee import TokenAmount from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers import ControllerBase, ControllerConfigBase -from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, TripleBarrierConfig -from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction +from hummingbot.strategy_v2.executors.data_types import ConnectorPair +from hummingbot.strategy_v2.executors.grid_executor.data_types import GridExecutorConfig +from hummingbot.strategy_v2.executors.position_executor.data_types import TripleBarrierConfig +from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction from hummingbot.strategy_v2.models.executors_info import ExecutorInfo -from hummingbot.strategy_v2.utils.distributions import Distributions - - -class GridRange(BaseModel): - id: str - start_price: Decimal - end_price: Decimal - total_amount_pct: Decimal - side: TradeType = TradeType.BUY - open_order_type: OrderType = OrderType.LIMIT_MAKER - take_profit_order_type: OrderType = OrderType.LIMIT - active: bool = True class GridStrikeConfig(ControllerConfigBase): """ Configuration required to run the GridStrike strategy for one connector and trading pair. """ + controller_type: str = "generic" 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"))], - client_data=ClientFieldData(is_updatable=True)) + + # Account configuration + leverage: int = 20 position_mode: PositionMode = PositionMode.HEDGE - leverage: int = 1 - time_limit: Optional[int] = Field(default=60 * 60 * 24 * 2, client_data=ClientFieldData(is_updatable=True)) - activation_bounds: Decimal = Field(default=Decimal("0.01"), client_data=ClientFieldData(is_updatable=True)) - min_spread_between_orders: Optional[Decimal] = Field(default=None, - client_data=ClientFieldData(is_updatable=True)) - min_order_amount: Optional[Decimal] = Field(default=Decimal("1"), - client_data=ClientFieldData(is_updatable=True)) - max_open_orders: int = Field(default=5, client_data=ClientFieldData(is_updatable=True)) - grid_range_update_interval: int = Field(default=60, client_data=ClientFieldData(is_updatable=True)) - extra_balance_base_usd: Decimal = Decimal("10") + + # Boundaries + connector_name: str = "binance_perpetual" + trading_pair: str = "WLD-USDT" + side: TradeType = TradeType.BUY + start_price: Decimal = Field(default=Decimal("0.58"), json_schema_extra={"is_updatable": True}) + end_price: Decimal = Field(default=Decimal("0.95"), json_schema_extra={"is_updatable": True}) + limit_price: Decimal = Field(default=Decimal("0.55"), json_schema_extra={"is_updatable": True}) + + # Profiling + total_amount_quote: Decimal = Field(default=Decimal("1000"), json_schema_extra={"is_updatable": True}) + min_spread_between_orders: Optional[Decimal] = Field(default=Decimal("0.001"), json_schema_extra={"is_updatable": True}) + min_order_amount_quote: Optional[Decimal] = Field(default=Decimal("5"), json_schema_extra={"is_updatable": True}) + + # Execution + max_open_orders: int = Field(default=2, json_schema_extra={"is_updatable": True}) + max_orders_per_batch: Optional[int] = Field(default=1, json_schema_extra={"is_updatable": True}) + order_frequency: int = Field(default=3, json_schema_extra={"is_updatable": True}) + activation_bounds: Optional[Decimal] = Field(default=None, json_schema_extra={"is_updatable": True}) + keep_position: bool = Field(default=False, json_schema_extra={"is_updatable": True}) + + # Risk Management + triple_barrier_config: TripleBarrierConfig = TripleBarrierConfig( + take_profit=Decimal("0.001"), + open_order_type=OrderType.LIMIT_MAKER, + take_profit_order_type=OrderType.LIMIT_MAKER, + ) def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]: if self.connector_name not in markets: @@ -58,16 +59,6 @@ class GridStrikeConfig(ControllerConfigBase): return markets -class GridLevel(BaseModel): - id: str - price: Decimal - amount: Decimal - step: Decimal - side: TradeType - open_order_type: OrderType - take_profit_order_type: OrderType - - class GridStrike(ControllerBase): def __init__(self, config: GridStrikeConfig, *args, **kwargs): super().__init__(config, *args, **kwargs) @@ -75,151 +66,134 @@ class GridStrike(ControllerBase): self._last_grid_levels_update = 0 self.trading_rules = None self.grid_levels = [] + self.initialize_rate_sources() - 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() - step_proposed = max(self.trading_rules.min_price_increment, spread_between_orders) - else: - step_proposed = self.trading_rules.min_price_increment - amount_proposed = max(self.trading_rules.min_notional_size, self.config.min_order_amount) if \ - self.config.min_order_amount else self.trading_rules.min_order_size - for grid_range in self.config.grid_ranges: - if grid_range.active: - total_amount = grid_range.total_amount_pct * self.config.total_amount_quote - theoretical_orders_by_step = (grid_range.end_price - grid_range.start_price) / step_proposed - theoretical_orders_by_amount = total_amount / amount_proposed - 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() - grid_levels.append(GridLevel(id=f"{grid_range.id}_P{i}", - price=price_quantized, - amount=amount_quantized, - step=step, side=grid_range.side, - open_order_type=grid_range.open_order_type, - take_profit_order_type=grid_range.take_profit_order_type, - )) - return grid_levels + def initialize_rate_sources(self): + self.market_data_provider.initialize_rate_sources([ConnectorPair(connector_name=self.config.connector_name, + trading_pair=self.config.trading_pair)]) - def get_balance_requirements(self) -> List[TokenAmount]: - if "perpetual" in self.config.connector_name: - return [] - base_currency = self.config.trading_pair.split("-")[0] - return [TokenAmount(base_currency, self.config.extra_balance_base_usd / self.get_mid_price())] - - def get_mid_price(self) -> Decimal: - return self.market_data_provider.get_price_by_type( - self.config.connector_name, - self.config.trading_pair, - PriceType.MidPrice - ) - - def active_executors(self, is_trading: bool) -> List[ExecutorInfo]: + def active_executors(self) -> List[ExecutorInfo]: return [ executor for executor in self.executors_info - if executor.is_active and executor.is_trading == is_trading + if executor.is_active ] + def is_inside_bounds(self, price: Decimal) -> bool: + return self.config.start_price <= price <= self.config.end_price + def determine_executor_actions(self) -> List[ExecutorAction]: - if self.market_data_provider.time() - self._last_grid_levels_update > 60: - self._last_grid_levels_update = self.market_data_provider.time() - self.grid_levels = self._calculate_grid_config() - return self.determine_create_executor_actions() + self.determine_stop_executor_actions() + mid_price = self.market_data_provider.get_price_by_type( + self.config.connector_name, self.config.trading_pair, PriceType.MidPrice) + if len(self.active_executors()) == 0 and self.is_inside_bounds(mid_price): + return [CreateExecutorAction( + controller_id=self.config.id, + executor_config=GridExecutorConfig( + timestamp=self.market_data_provider.time(), + connector_name=self.config.connector_name, + trading_pair=self.config.trading_pair, + start_price=self.config.start_price, + end_price=self.config.end_price, + leverage=self.config.leverage, + limit_price=self.config.limit_price, + side=self.config.side, + total_amount_quote=self.config.total_amount_quote, + min_spread_between_orders=self.config.min_spread_between_orders, + min_order_amount_quote=self.config.min_order_amount_quote, + max_open_orders=self.config.max_open_orders, + max_orders_per_batch=self.config.max_orders_per_batch, + order_frequency=self.config.order_frequency, + activation_bounds=self.config.activation_bounds, + triple_barrier_config=self.config.triple_barrier_config, + level_id=None, + keep_position=self.config.keep_position, + ))] + return [] async def update_processed_data(self): - mid_price = self.get_mid_price() - self.processed_data.update({ - "mid_price": mid_price, - "active_executors_order_placed": self.active_executors(is_trading=False), - "active_executors_order_trading": self.active_executors(is_trading=True), - "long_activation_bounds": mid_price * (1 - self.config.activation_bounds), - "short_activation_bounds": mid_price * (1 + self.config.activation_bounds), - }) + pass - def determine_create_executor_actions(self) -> List[ExecutorAction]: - mid_price = self.processed_data["mid_price"] - long_activation_bounds = self.processed_data["long_activation_bounds"] - short_activation_bounds = self.processed_data["short_activation_bounds"] - levels_allowed = [] - for level in self.grid_levels: - if (level.side == TradeType.BUY and level.price >= long_activation_bounds) or \ - (level.side == TradeType.SELL and level.price <= short_activation_bounds): - levels_allowed.append(level) - active_executors = self.processed_data["active_executors_order_placed"] + \ - self.processed_data["active_executors_order_trading"] - active_executors_level_id = [executor.custom_info["level_id"] for executor in active_executors] - levels_allowed = sorted([level for level in levels_allowed if level.id not in active_executors_level_id], - key=lambda level: abs(level.price - mid_price)) - levels_allowed = levels_allowed[:self.config.max_open_orders] - create_actions = [] - for level in levels_allowed: - if level.side == TradeType.BUY and level.price > mid_price: - entry_price = mid_price - take_profit = max(level.step * 2, ((level.price - mid_price) / mid_price) + level.step) - trailing_stop = None - # trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) - # trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2) - elif level.side == TradeType.SELL and level.price < mid_price: - entry_price = mid_price - take_profit = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) - # trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) - # trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2) - trailing_stop = None - else: - entry_price = level.price - take_profit = level.step - trailing_stop = None - create_actions.append(CreateExecutorAction(controller_id=self.config.id, - executor_config=PositionExecutorConfig( - timestamp=self.market_data_provider.time(), - connector_name=self.config.connector_name, - trading_pair=self.config.trading_pair, - entry_price=entry_price, - amount=level.amount, - leverage=self.config.leverage, - side=level.side, - level_id=level.id, - activation_bounds=[self.config.activation_bounds, - self.config.activation_bounds], - triple_barrier_config=TripleBarrierConfig( - take_profit=take_profit, - time_limit=self.config.time_limit, - open_order_type=OrderType.LIMIT_MAKER, - take_profit_order_type=level.take_profit_order_type, - trailing_stop=trailing_stop, - )))) - return create_actions - - def determine_stop_executor_actions(self) -> List[ExecutorAction]: - long_activation_bounds = self.processed_data["long_activation_bounds"] - short_activation_bounds = self.processed_data["short_activation_bounds"] - active_executors_order_placed = self.processed_data["active_executors_order_placed"] - non_active_ranges = [grid_range.id for grid_range in self.config.grid_ranges if not grid_range.active] - active_executor_of_non_active_ranges = [executor.id for executor in self.executors_info if - executor.is_active and - executor.custom_info["level_id"].split("_")[0] in non_active_ranges] - long_executors_to_stop = [executor.id for executor in active_executors_order_placed if - executor.side == TradeType.BUY and - executor.config.entry_price <= long_activation_bounds] - 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) - return [StopExecutorAction(controller_id=self.config.id, executor_id=executor) for executor in - list(executors_id_to_stop)] + def to_format_status(self) -> List[str]: + status = [] + mid_price = self.market_data_provider.get_price_by_type( + self.config.connector_name, self.config.trading_pair, PriceType.MidPrice) + # Define standard box width for consistency + box_width = 114 + # Top Grid Configuration box with simple borders + status.append("┌" + "─" * box_width + "┐") + # First line: Grid Configuration and Mid Price + left_section = "Grid Configuration:" + padding = box_width - len(left_section) - 4 # -4 for the border characters and spacing + config_line1 = f"│ {left_section}{' ' * padding}" + padding2 = box_width - len(config_line1) + 1 # +1 for correct right border alignment + config_line1 += " " * padding2 + "│" + status.append(config_line1) + # Second line: Configuration parameters + config_line2 = f"│ Start: {self.config.start_price:.4f} │ End: {self.config.end_price:.4f} │ Side: {self.config.side} │ Limit: {self.config.limit_price:.4f} │ Mid Price: {mid_price:.4f} │" + padding = box_width - len(config_line2) + 1 # +1 for correct right border alignment + config_line2 += " " * padding + "│" + status.append(config_line2) + # Third line: Max orders and Inside bounds + config_line3 = f"│ Max Orders: {self.config.max_open_orders} │ Inside bounds: {1 if self.is_inside_bounds(mid_price) else 0}" + padding = box_width - len(config_line3) + 1 # +1 for correct right border alignment + config_line3 += " " * padding + "│" + status.append(config_line3) + status.append("└" + "─" * box_width + "┘") + for level in self.active_executors(): + # Define column widths for perfect alignment + col_width = box_width // 3 # Dividing the total width by 3 for equal columns + total_width = box_width + # Grid Status header - use long line and running status + status_header = f"Grid Status: {level.id} (RunnableStatus.RUNNING)" + status_line = f"┌ {status_header}" + "─" * (total_width - len(status_header) - 2) + "┐" + status.append(status_line) + # Calculate exact column widths for perfect alignment + col1_end = col_width + # Column headers + header_line = "│ Level Distribution" + " " * (col1_end - 20) + "│" + header_line += " Order Statistics" + " " * (col_width - 18) + "│" + header_line += " Performance Metrics" + " " * (col_width - 21) + "│" + status.append(header_line) + # Data for the three columns + level_dist_data = [ + f"NOT_ACTIVE: {len(level.custom_info['levels_by_state'].get('NOT_ACTIVE', []))}", + f"OPEN_ORDER_PLACED: {len(level.custom_info['levels_by_state'].get('OPEN_ORDER_PLACED', []))}", + f"OPEN_ORDER_FILLED: {len(level.custom_info['levels_by_state'].get('OPEN_ORDER_FILLED', []))}", + f"CLOSE_ORDER_PLACED: {len(level.custom_info['levels_by_state'].get('CLOSE_ORDER_PLACED', []))}", + f"COMPLETE: {len(level.custom_info['levels_by_state'].get('COMPLETE', []))}" + ] + order_stats_data = [ + f"Total: {sum(len(level.custom_info[k]) for k in ['filled_orders', 'failed_orders', 'canceled_orders'])}", + f"Filled: {len(level.custom_info['filled_orders'])}", + f"Failed: {len(level.custom_info['failed_orders'])}", + f"Canceled: {len(level.custom_info['canceled_orders'])}" + ] + perf_metrics_data = [ + f"Buy Vol: {level.custom_info['realized_buy_size_quote']:.4f}", + f"Sell Vol: {level.custom_info['realized_sell_size_quote']:.4f}", + f"R. PnL: {level.custom_info['realized_pnl_quote']:.4f}", + f"R. Fees: {level.custom_info['realized_fees_quote']:.4f}", + f"P. PnL: {level.custom_info['position_pnl_quote']:.4f}", + f"Position: {level.custom_info['position_size_quote']:.4f}" + ] + # Build rows with perfect alignment + max_rows = max(len(level_dist_data), len(order_stats_data), len(perf_metrics_data)) + for i in range(max_rows): + col1 = level_dist_data[i] if i < len(level_dist_data) else "" + col2 = order_stats_data[i] if i < len(order_stats_data) else "" + col3 = perf_metrics_data[i] if i < len(perf_metrics_data) else "" + row = "│ " + col1 + row += " " * (col1_end - len(col1) - 2) # -2 for the "│ " at the start + row += "│ " + col2 + row += " " * (col_width - len(col2) - 2) # -2 for the "│ " before col2 + row += "│ " + col3 + row += " " * (col_width - len(col3) - 2) # -2 for the "│ " before col3 + row += "│" + status.append(row) + # Liquidity line with perfect alignment + status.append("├" + "─" * total_width + "┤") + liquidity_line = f"│ Open Liquidity: {level.custom_info['open_liquidity_placed']:.4f} │ Close Liquidity: {level.custom_info['close_liquidity_placed']:.4f} │" + liquidity_line += " " * (total_width - len(liquidity_line) + 1) # +1 for correct right border alignment + liquidity_line += "│" + status.append(liquidity_line) + status.append("└" + "─" * total_width + "┘") + return status diff --git a/bots/controllers/generic/spot_perp_arbitrage.py b/bots/controllers/generic/spot_perp_arbitrage.py deleted file mode 100644 index f2f434b..0000000 --- a/bots/controllers/generic/spot_perp_arbitrage.py +++ /dev/null @@ -1,196 +0,0 @@ -import time -from decimal import Decimal -from typing import Dict, List, Set - -import pandas as pd -from pydantic import Field, validator - -from hummingbot.client.config.config_data_types import ClientFieldData -from hummingbot.client.ui.interface_utils import format_df_for_printout -from hummingbot.core.data_type.common import PriceType, TradeType, PositionAction, OrderType -from hummingbot.data_feed.candles_feed.data_types import CandlesConfig -from hummingbot.strategy_v2.controllers.controller_base import ControllerBase, ControllerConfigBase -from hummingbot.strategy_v2.executors.data_types import ConnectorPair -from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, \ - TripleBarrierConfig -from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig -from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction - - -class SpotPerpArbitrageConfig(ControllerConfigBase): - controller_name: str = "spot_perp_arbitrage" - candles_config: List[CandlesConfig] = [] - spot_connector: str = Field( - default="binance", - client_data=ClientFieldData( - prompt=lambda e: "Enter the spot connector: ", - prompt_on_new=True - )) - spot_trading_pair: str = Field( - default="DOGE-USDT", - client_data=ClientFieldData( - prompt=lambda e: "Enter the spot trading pair: ", - prompt_on_new=True - )) - perp_connector: str = Field( - default="binance_perpetual", - client_data=ClientFieldData( - prompt=lambda e: "Enter the perp connector: ", - prompt_on_new=True - )) - perp_trading_pair: str = Field( - default="DOGE-USDT", - client_data=ClientFieldData( - prompt=lambda e: "Enter the perp trading pair: ", - prompt_on_new=True - )) - profitability: Decimal = Field( - default=0.002, - client_data=ClientFieldData( - prompt=lambda e: "Enter the minimum profitability: ", - prompt_on_new=True - )) - position_size_quote: float = Field( - default=50, - client_data=ClientFieldData( - prompt=lambda e: "Enter the position size in quote currency: ", - prompt_on_new=True - )) - - def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]: - if self.spot_connector not in markets: - markets[self.spot_connector] = set() - markets[self.spot_connector].add(self.spot_trading_pair) - if self.perp_connector not in markets: - markets[self.perp_connector] = set() - markets[self.perp_connector].add(self.perp_trading_pair) - return markets - - -class SpotPerpArbitrage(ControllerBase): - - def __init__(self, config: SpotPerpArbitrageConfig, *args, **kwargs): - self.config = config - super().__init__(config, *args, **kwargs) - - @property - def spot_connector(self): - return self.market_data_provider.connectors[self.config.spot_connector] - - @property - def perp_connector(self): - return self.market_data_provider.connectors[self.config.perp_connector] - - def get_current_profitability_after_fees(self): - """ - This methods compares the profitability of buying at market in the two exchanges. If the side is TradeType.BUY - means that the operation is long on connector 1 and short on connector 2. - """ - spot_trading_pair = self.config.spot_trading_pair - perp_trading_pair = self.config.perp_trading_pair - - connector_spot_price = Decimal(self.market_data_provider.get_price_for_quote_volume( - connector_name=self.config.spot_connector, - trading_pair=spot_trading_pair, - quote_volume=self.config.position_size_quote, - is_buy=True, - ).result_price) - connector_perp_price = Decimal(self.market_data_provider.get_price_for_quote_volume( - connector_name=self.config.spot_connector, - trading_pair=perp_trading_pair, - quote_volume=self.config.position_size_quote, - is_buy=False, - ).result_price) - estimated_fees_spot_connector = self.spot_connector.get_fee( - base_currency=spot_trading_pair.split("-")[0], - quote_currency=spot_trading_pair.split("-")[1], - order_type=OrderType.MARKET, - order_side=TradeType.BUY, - amount=self.config.position_size_quote / float(connector_spot_price), - price=connector_spot_price, - is_maker=False, - ).percent - estimated_fees_perp_connector = self.perp_connector.get_fee( - base_currency=perp_trading_pair.split("-")[0], - quote_currency=perp_trading_pair.split("-")[1], - order_type=OrderType.MARKET, - order_side=TradeType.BUY, - amount=self.config.position_size_quote / float(connector_perp_price), - price=connector_perp_price, - is_maker=False, - position_action=PositionAction.OPEN - ).percent - - estimated_trade_pnl_pct = (connector_perp_price - connector_spot_price) / connector_spot_price - return estimated_trade_pnl_pct - estimated_fees_spot_connector - estimated_fees_perp_connector - - def is_active_arbitrage(self): - executors = self.filter_executors( - executors=self.executors_info, - filter_func=lambda e: e.is_active - ) - return len(executors) > 0 - - def current_pnl_pct(self): - executors = self.filter_executors( - executors=self.executors_info, - filter_func=lambda e: e.is_active - ) - filled_amount = sum(e.filled_amount_quote for e in executors) - return sum(e.net_pnl_quote for e in executors) / filled_amount if filled_amount > 0 else 0 - - async def update_processed_data(self): - self.processed_data = { - "profitability": self.get_current_profitability_after_fees(), - "active_arbitrage": self.is_active_arbitrage(), - "current_pnl": self.current_pnl_pct() - } - - def determine_executor_actions(self) -> List[ExecutorAction]: - executor_actions = [] - executor_actions.extend(self.create_new_arbitrage_actions()) - executor_actions.extend(self.stop_arbitrage_actions()) - return executor_actions - - def create_new_arbitrage_actions(self): - create_actions = [] - if not self.processed_data["active_arbitrage"] and self.processed_data["profitability"] > self.config.profitability: - mid_price = self.market_data_provider.get_price_by_type(self.config.spot_connector, self.config.spot_trading_pair, PriceType.MidPrice) - create_actions.append(CreateExecutorAction( - controller_id=self.config.id, - executor_config=PositionExecutorConfig( - timestamp=self.market_data_provider.time(), - connector_name=self.config.spot_connector, - trading_pair=self.config.spot_trading_pair, - side=TradeType.BUY, - amount=Decimal(self.config.position_size_quote) / mid_price, - triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), - ) - )) - create_actions.append(CreateExecutorAction( - controller_id=self.config.id, - executor_config=PositionExecutorConfig( - timestamp=self.market_data_provider.time(), - connector_name=self.config.perp_connector, - trading_pair=self.config.perp_trading_pair, - side=TradeType.SELL, - amount=Decimal(self.config.position_size_quote) / mid_price, - triple_barrier_config=TripleBarrierConfig(open_order_type=OrderType.MARKET), - )) - ) - return create_actions - - def stop_arbitrage_actions(self): - stop_actions = [] - if self.processed_data["current_pnl"] > 0.003: - executors = self.filter_executors( - executors=self.executors_info, - filter_func=lambda e: e.is_active - ) - for executor in executors: - stop_actions.append(StopExecutorAction(controller_id=self.config.id, executor_id=executor.id)) - - def to_format_status(self) -> List[str]: - return [f"Current profitability: {self.processed_data['profitability']} | Min profitability: {self.config.profitability}", - f"Active arbitrage: {self.processed_data['active_arbitrage']}", - f"Current PnL: {self.processed_data['current_pnl']}"] diff --git a/bots/controllers/generic/xemm_multiple_levels.py b/bots/controllers/generic/xemm_multiple_levels.py index d12bb52..9983e11 100644 --- a/bots/controllers/generic/xemm_multiple_levels.py +++ b/bots/controllers/generic/xemm_multiple_levels.py @@ -3,9 +3,8 @@ from decimal import Decimal from typing import Dict, List, Set import pandas as pd -from pydantic import Field, validator +from pydantic import Field, field_validator -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.client.ui.interface_utils import format_df_for_printout from hummingbot.core.data_type.common import PriceType, TradeType from hummingbot.data_feed.candles_feed.data_types import CandlesConfig @@ -19,62 +18,40 @@ class XEMMMultipleLevelsConfig(ControllerConfigBase): controller_name: str = "xemm_multiple_levels" candles_config: List[CandlesConfig] = [] maker_connector: str = Field( - default="kucoin", - client_data=ClientFieldData( - prompt=lambda e: "Enter the maker connector: ", - prompt_on_new=True - )) + default="mexc", + json_schema_extra={"prompt": "Enter the maker connector: ", "prompt_on_new": True}) maker_trading_pair: str = Field( - default="LBR-USDT", - client_data=ClientFieldData( - prompt=lambda e: "Enter the maker trading pair: ", - prompt_on_new=True - )) + default="PEPE-USDT", + json_schema_extra={"prompt": "Enter the maker trading pair: ", "prompt_on_new": True}) taker_connector: str = Field( - default="okx", - client_data=ClientFieldData( - prompt=lambda e: "Enter the taker connector: ", - prompt_on_new=True - )) + default="binance", + json_schema_extra={"prompt": "Enter the taker connector: ", "prompt_on_new": True}) taker_trading_pair: str = Field( - default="LBR-USDT", - client_data=ClientFieldData( - prompt=lambda e: "Enter the taker trading pair: ", - prompt_on_new=True - )) + default="PEPE-USDT", + json_schema_extra={"prompt": "Enter the taker trading pair: ", "prompt_on_new": True}) buy_levels_targets_amount: List[List[Decimal]] = Field( default="0.003,10-0.006,20-0.009,30", - client_data=ClientFieldData( - prompt=lambda e: "Enter the buy levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", - prompt_on_new=True - )) + json_schema_extra={ + "prompt": "Enter the buy levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", + "prompt_on_new": True}) sell_levels_targets_amount: List[List[Decimal]] = Field( default="0.003,10-0.006,20-0.009,30", - client_data=ClientFieldData( - prompt=lambda e: "Enter the sell levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", - prompt_on_new=True - )) + json_schema_extra={ + "prompt": "Enter the sell levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", + "prompt_on_new": True}) min_profitability: Decimal = Field( - default=0.002, - client_data=ClientFieldData( - prompt=lambda e: "Enter the minimum profitability: ", - prompt_on_new=True - )) + default=0.003, + json_schema_extra={"prompt": "Enter the minimum profitability: ", "prompt_on_new": True}) max_profitability: Decimal = Field( default=0.01, - client_data=ClientFieldData( - prompt=lambda e: "Enter the maximum profitability: ", - prompt_on_new=True - )) + json_schema_extra={"prompt": "Enter the maximum profitability: ", "prompt_on_new": True}) max_executors_imbalance: int = Field( default=1, - client_data=ClientFieldData( - prompt=lambda e: "Enter the maximum executors imbalance: ", - prompt_on_new=True - )) + json_schema_extra={"prompt": "Enter the maximum executors imbalance: ", "prompt_on_new": True}) - @validator("buy_levels_targets_amount", "sell_levels_targets_amount", pre=True, always=True) - def validate_levels_targets_amount(cls, v, values): + @field_validator("buy_levels_targets_amount", "sell_levels_targets_amount", mode="before") + @classmethod + def validate_levels_targets_amount(cls, v): if isinstance(v, str): v = [list(map(Decimal, x.split(","))) for x in v.split("-")] return v @@ -124,6 +101,8 @@ class XEMMMultipleLevels(ControllerBase): active_buy_executors_target = [e.config.target_profitability == target_profitability for e in active_buy_executors] if len(active_buy_executors_target) == 0 and imbalance < self.config.max_executors_imbalance: + min_profitability = target_profitability - self.config.min_profitability + max_profitability = target_profitability + self.config.max_profitability config = XEMMExecutorConfig( controller_id=self.config.id, timestamp=self.market_data_provider.time(), @@ -133,14 +112,16 @@ class XEMMMultipleLevels(ControllerBase): trading_pair=self.config.taker_trading_pair), maker_side=TradeType.BUY, order_amount=amount / mid_price, - min_profitability=self.config.min_profitability, + min_profitability=min_profitability, target_profitability=target_profitability, - max_profitability=self.config.max_profitability + max_profitability=max_profitability ) executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id)) for target_profitability, amount in self.sell_levels_targets_amount: active_sell_executors_target = [e.config.target_profitability == target_profitability for e in active_sell_executors] if len(active_sell_executors_target) == 0 and imbalance > -self.config.max_executors_imbalance: + min_profitability = target_profitability - self.config.min_profitability + max_profitability = target_profitability + self.config.max_profitability config = XEMMExecutorConfig( controller_id=self.config.id, timestamp=time.time(), @@ -150,9 +131,9 @@ class XEMMMultipleLevels(ControllerBase): trading_pair=self.config.maker_trading_pair), maker_side=TradeType.SELL, order_amount=amount / mid_price, - min_profitability=self.config.min_profitability, + min_profitability=min_profitability, target_profitability=target_profitability, - max_profitability=self.config.max_profitability + max_profitability=max_profitability ) executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id)) return executor_actions diff --git a/bots/controllers/market_making/dman_maker_v2.py b/bots/controllers/market_making/dman_maker_v2.py index 5fe15cc..2002fdd 100644 --- a/bots/controllers/market_making/dman_maker_v2.py +++ b/bots/controllers/market_making/dman_maker_v2.py @@ -2,9 +2,8 @@ from decimal import Decimal from typing import List, Optional import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, 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 ( @@ -25,38 +24,15 @@ class DManMakerV2Config(MarketMakingControllerConfigBase): # 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: ")) + json_schema_extra={"prompt": "Enter a comma-separated list of spreads for each DCA level: ", "prompt_on_new": True}) 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)) + json_schema_extra={"prompt": "Enter a comma-separated list of amounts for each DCA level: ", "prompt_on_new": True}) + top_executor_refresh_time: Optional[float] = Field(default=None, json_schema_extra={"is_updatable": True}) + executor_activation_bounds: Optional[List[Decimal]] = Field(default=None, json_schema_extra={"is_updatable": True}) - @validator("executor_activation_bounds", pre=True, always=True) + @field_validator("executor_activation_bounds", mode="before") + @classmethod def parse_activation_bounds(cls, v): if isinstance(v, list): return [Decimal(val) for val in v] @@ -66,8 +42,9 @@ class DManMakerV2Config(MarketMakingControllerConfigBase): return [Decimal(val) for val in v.split(",")] return v - @validator('dca_spreads', pre=True, always=True) - def parse_spreads(cls, v): + @field_validator('dca_spreads', mode="before") + @classmethod + def parse_dca_spreads(cls, v): if v is None: return [] if isinstance(v, str): @@ -76,15 +53,16 @@ class DManMakerV2Config(MarketMakingControllerConfigBase): 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): + @field_validator('dca_amounts', mode="before") + @classmethod + def parse_and_validate_dca_amounts(cls, v, validation_info): if v is None or v == "": - return [1 for _ in values[values['dca_spreads']]] + return [1 for _ in validation_info.data['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']): + elif isinstance(v, list) and len(v) != len(validation_info.data['dca_spreads']): raise ValueError( - f"The number of {field.name} must match the number of {values['dca_spreads']}.") + f"The number of dca amounts must match the number of {validation_info.data['dca_spreads']}.") return v @@ -98,11 +76,11 @@ class DManMakerV2(MarketMakingControllerBase): 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 self.market_data_provider.time() - executor.timestamp > self.config.top_executor_refresh_time return False def order_level_refresh_condition(self, executor): - return self.market_data_provider.time() - executor.timestamp > self.config.executor_refresh_time * 1000 + return self.market_data_provider.time() - executor.timestamp > self.config.executor_refresh_time def executors_to_refresh(self) -> List[ExecutorAction]: executors_to_refresh = self.filter_executors( diff --git a/bots/controllers/market_making/pmm_dynamic.py b/bots/controllers/market_making/pmm_dynamic.py index 3b90fad..612f7c9 100644 --- a/bots/controllers/market_making/pmm_dynamic.py +++ b/bots/controllers/market_making/pmm_dynamic.py @@ -2,9 +2,9 @@ from decimal import Decimal from typing import List import pandas_ta as ta # noqa: F401 -from pydantic import Field, validator +from pydantic import Field, field_validator +from pydantic_core.core_schema import ValidationInfo -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.market_making_controller_base import ( MarketMakingControllerBase, @@ -14,69 +14,60 @@ from hummingbot.strategy_v2.executors.position_executor.data_types import Positi class PMMDynamicControllerConfig(MarketMakingControllerConfigBase): - controller_name = "pmm_dynamic" + controller_name: str = "pmm_dynamic" candles_config: List[CandlesConfig] = [] buy_spreads: List[float] = Field( default="1,2,4", - client_data=ClientFieldData( - is_updatable=True, - prompt_on_new=True, - prompt=lambda mi: "Enter a comma-separated list of buy spreads (e.g., '0.01, 0.02'):")) + json_schema_extra={ + "prompt": "Enter a comma-separated list of buy spreads measured in units of volatility(e.g., '1, 2'): ", + "prompt_on_new": True, "is_updatable": True} + ) sell_spreads: List[float] = Field( default="1,2,4", - client_data=ClientFieldData( - is_updatable=True, - prompt_on_new=True, - prompt=lambda mi: "Enter a comma-separated list of sell spreads (e.g., '0.01, 0.02'):")) + json_schema_extra={ + "prompt": "Enter a comma-separated list of sell spreads measured in units of volatility(e.g., '1, 2'): ", + "prompt_on_new": True, "is_updatable": True} + ) candles_connector: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", + "prompt_on_new": True}) candles_trading_pair: str = Field( default=None, - client_data=ClientFieldData( - prompt_on_new=True, - prompt=lambda mi: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", ) - ) + json_schema_extra={ + "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", + "prompt_on_new": True}) interval: str = Field( default="3m", - client_data=ClientFieldData( - prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", - prompt_on_new=False)) - + json_schema_extra={ + "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", + "prompt_on_new": True}) macd_fast: int = Field( - default=12, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD fast length: ", - prompt_on_new=True)) + default=21, + json_schema_extra={"prompt": "Enter the MACD fast period: ", "prompt_on_new": True}) macd_slow: int = Field( - default=26, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD slow length: ", - prompt_on_new=True)) + default=42, + json_schema_extra={"prompt": "Enter the MACD slow period: ", "prompt_on_new": True}) macd_signal: int = Field( default=9, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the MACD signal length: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the MACD signal period: ", "prompt_on_new": True}) natr_length: int = Field( default=14, - client_data=ClientFieldData( - prompt=lambda mi: "Enter the NATR length: ", - prompt_on_new=True)) + json_schema_extra={"prompt": "Enter the NATR length: ", "prompt_on_new": True}) - @validator("candles_connector", pre=True, always=True) - def set_candles_connector(cls, v, values): + @field_validator("candles_connector", mode="before") + @classmethod + def set_candles_connector(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("connector_name") + return validation_info.data.get("connector_name") return v - @validator("candles_trading_pair", pre=True, always=True) - def set_candles_trading_pair(cls, v, values): + @field_validator("candles_trading_pair", mode="before") + @classmethod + def set_candles_trading_pair(cls, v, validation_info: ValidationInfo): if v is None or v == "": - return values.get("trading_pair") + return validation_info.data.get("trading_pair") return v @@ -87,7 +78,7 @@ class PMMDynamicController(MarketMakingControllerBase): """ def __init__(self, config: PMMDynamicControllerConfig, *args, **kwargs): self.config = config - self.max_records = max(config.macd_slow, config.macd_fast, config.macd_signal, config.natr_length) + 200 + self.max_records = max(config.macd_slow, config.macd_fast, config.macd_signal, config.natr_length) + 100 if len(self.config.candles_config) == 0: self.config.candles_config = [CandlesConfig( connector=config.candles_connector, diff --git a/bots/controllers/market_making/pmm_simple.py b/bots/controllers/market_making/pmm_simple.py index a773d14..6b09f33 100644 --- a/bots/controllers/market_making/pmm_simple.py +++ b/bots/controllers/market_making/pmm_simple.py @@ -3,7 +3,6 @@ from typing import List from pydantic import Field -from hummingbot.client.config.config_data_types import ClientFieldData from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.strategy_v2.controllers.market_making_controller_base import ( MarketMakingControllerBase, @@ -13,9 +12,9 @@ from hummingbot.strategy_v2.executors.position_executor.data_types import Positi class PMMSimpleConfig(MarketMakingControllerConfigBase): - controller_name = "pmm_simple" + controller_name: str = "pmm_simple" # As this controller is a simple version of the PMM, we are not using the candles feed - candles_config: List[CandlesConfig] = Field(default=[], client_data=ClientFieldData(prompt_on_new=False)) + candles_config: List[CandlesConfig] = Field(default=[]) class PMMSimpleController(MarketMakingControllerBase):