(feat) update controllers

This commit is contained in:
cardosofede
2025-04-11 16:01:45 -03:00
parent e1d8d45339
commit 97c450e778
10 changed files with 396 additions and 689 deletions

View File

@@ -1,9 +1,9 @@
from typing import List from typing import List
import pandas_ta as ta # noqa: F401 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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
DirectionalTradingControllerBase, DirectionalTradingControllerBase,
@@ -12,56 +12,42 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo
class BollingerV1ControllerConfig(DirectionalTradingControllerConfigBase): class BollingerV1ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name = "bollinger_v1" controller_name: str = "bollinger_v1"
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
candles_connector: str = Field( candles_connector: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",
prompt=lambda mi: "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( candles_trading_pair: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ",
prompt=lambda mi: "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( interval: str = Field(
default="3m", default="3m",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ",
prompt_on_new=False)) "prompt_on_new": True})
bb_length: int = Field( bb_length: int = Field(
default=100, default=100,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the Bollinger Bands length: ", bb_std: float = Field(default=2.0)
prompt_on_new=True)) bb_long_threshold: float = Field(default=0.0)
bb_std: float = Field( bb_short_threshold: float = Field(default=1.0)
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))
@validator("candles_connector", pre=True, always=True) @field_validator("candles_connector", mode="before")
def set_candles_connector(cls, v, values): @classmethod
def set_candles_connector(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("connector_name") return validation_info.data.get("connector_name")
return v return v
@validator("candles_trading_pair", pre=True, always=True) @field_validator("candles_trading_pair", mode="before")
def set_candles_trading_pair(cls, v, values): @classmethod
def set_candles_trading_pair(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("trading_pair") return validation_info.data.get("trading_pair")
return v return v

View File

@@ -3,9 +3,9 @@ from decimal import Decimal
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import pandas_ta as ta # noqa: F401 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.core.data_type.common import TradeType
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
@@ -21,75 +21,63 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase):
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
candles_connector: str = Field( candles_connector: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",
prompt=lambda mi: "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( candles_trading_pair: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ",
prompt=lambda mi: "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( interval: str = Field(
default="30m", default="3m",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ",
prompt_on_new=True)) "prompt_on_new": True})
bb_length: int = Field( bb_length: int = Field(
default=100, default=100,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the Bollinger Bands length: ", bb_std: float = Field(default=2.0)
prompt_on_new=True)) bb_long_threshold: float = Field(default=0.0)
bb_std: float = Field( bb_short_threshold: float = Field(default=1.0)
default=2.0, trailing_stop: Optional[TrailingStop] = Field(
client_data=ClientFieldData( default="0.015,0.005",
prompt=lambda mi: "Enter the Bollinger Bands standard deviation: ", json_schema_extra={
prompt_on_new=False)) "prompt": "Enter the trailing stop parameters (activation_price, trailing_delta) as a comma-separated list: ",
bb_long_threshold: float = Field( "prompt_on_new": True,
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))
dca_spreads: List[Decimal] = Field( dca_spreads: List[Decimal] = Field(
default="0.001,0.018,0.15,0.25", default="0.001,0.018,0.15,0.25",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the spreads for each DCA level (comma-separated) if dynamic_spread=True this value " "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%)" "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%) ", "and the spread is 0.2, the distance of the order to the current price will be 0.02 (2%) ",
prompt_on_new=True)) "prompt_on_new": True},
)
dca_amounts_pct: List[Decimal] = Field( dca_amounts_pct: List[Decimal] = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the amounts for each DCA level (as a percentage of the total balance, " "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. ", "comma-separated). Don't worry about the final sum, it will be normalized. ",
prompt_on_new=True)) "prompt_on_new": True},
)
dynamic_order_spread: bool = Field( dynamic_order_spread: bool = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={"prompt": "Do you want to make the spread dynamic? (Yes/No) ", "prompt_on_new": True})
prompt=lambda mi: "Do you want to make the spread dynamic? (Yes/No) ",
prompt_on_new=True))
dynamic_target: bool = Field( dynamic_target: bool = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={"prompt": "Do you want to make the target dynamic? (Yes/No) ", "prompt_on_new": True})
prompt=lambda mi: "Do you want to make the target dynamic? (Yes/No) ",
prompt_on_new=True))
activation_bounds: Optional[List[Decimal]] = Field( activation_bounds: Optional[List[Decimal]] = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the activation bounds for the orders " "prompt": "Enter the activation bounds for the orders (e.g., 0.01 activates the next order when the price is closer than 1%): ",
"(e.g., 0.01 activates the next order when the price is closer than 1%): ", "prompt_on_new": True,
prompt_on_new=True)) }
)
@validator("activation_bounds", pre=True, always=True) @field_validator("activation_bounds", mode="before")
@classmethod
def parse_activation_bounds(cls, v): def parse_activation_bounds(cls, v):
if isinstance(v, str): if isinstance(v, str):
if v == "": if v == "":
@@ -99,15 +87,17 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase):
return [Decimal(val) for val in v] return [Decimal(val) for val in v]
return v return v
@validator('dca_spreads', pre=True, always=True) @field_validator('dca_spreads', mode="before")
@classmethod
def validate_spreads(cls, v): def validate_spreads(cls, v):
if isinstance(v, str): if isinstance(v, str):
return [Decimal(val) for val in v.split(",")] return [Decimal(val) for val in v.split(",")]
return v return v
@validator('dca_amounts_pct', pre=True, always=True) @field_validator('dca_amounts_pct', mode="before")
def validate_amounts(cls, v, values): @classmethod
spreads = values.get("dca_spreads") def validate_amounts(cls, v, validation_info: ValidationInfo):
spreads = validation_info.data.get("dca_spreads")
if isinstance(v, str): if isinstance(v, str):
if v == "": if v == "":
return [Decimal('1.0') / len(spreads) for _ in spreads] 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 [Decimal('1.0') / len(spreads) for _ in spreads]
return v 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]]: 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 amounts_pct = self.dca_amounts_pct
if amounts_pct is None: 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] 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): class DManV3Controller(DirectionalTradingControllerBase):
""" """
@@ -201,8 +193,12 @@ class DManV3Controller(DirectionalTradingControllerBase):
prices = [price * (1 + spread * spread_multiplier) for spread in spread] prices = [price * (1 + spread * spread_multiplier) for spread in spread]
if self.config.dynamic_target: if self.config.dynamic_target:
stop_loss = self.config.stop_loss * spread_multiplier stop_loss = self.config.stop_loss * spread_multiplier
trailing_stop = TrailingStop(activation_price=self.config.trailing_stop.activation_price * spread_multiplier, if self.config.trailing_stop:
trailing_delta=self.config.trailing_stop.trailing_delta * 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)
else:
trailing_stop = None
else: else:
stop_loss = self.config.stop_loss stop_loss = self.config.stop_loss
trailing_stop = self.config.trailing_stop trailing_stop = self.config.trailing_stop

View File

@@ -1,9 +1,9 @@
from typing import List from typing import List
import pandas_ta as ta # noqa: F401 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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
DirectionalTradingControllerBase, DirectionalTradingControllerBase,
@@ -12,71 +12,51 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo
class MACDBBV1ControllerConfig(DirectionalTradingControllerConfigBase): class MACDBBV1ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name = "macd_bb_v1" controller_name: str = "macd_bb_v1"
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
candles_connector: str = Field( candles_connector: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",
prompt=lambda mi: "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( candles_trading_pair: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ",
prompt=lambda mi: "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( interval: str = Field(
default="3m", default="3m",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ",
prompt_on_new=False)) "prompt_on_new": True})
bb_length: int = Field( bb_length: int = Field(
default=100, default=100,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the Bollinger Bands length: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the Bollinger Bands length: ", bb_std: float = Field(default=2.0)
prompt_on_new=True)) bb_long_threshold: float = Field(default=0.0)
bb_std: float = Field( bb_short_threshold: float = Field(default=1.0)
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))
macd_fast: int = Field( macd_fast: int = Field(
default=21, default=21,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD fast period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD fast period: ",
prompt_on_new=True))
macd_slow: int = Field( macd_slow: int = Field(
default=42, default=42,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD slow period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD slow period: ",
prompt_on_new=True))
macd_signal: int = Field( macd_signal: int = Field(
default=9, default=9,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD signal period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD signal period: ",
prompt_on_new=True))
@validator("candles_connector", pre=True, always=True) @field_validator("candles_connector", mode="before")
def set_candles_connector(cls, v, values): @classmethod
def set_candles_connector(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("connector_name") return validation_info.data.get("connector_name")
return v return v
@validator("candles_trading_pair", pre=True, always=True) @field_validator("candles_trading_pair", mode="before")
def set_candles_trading_pair(cls, v, values): @classmethod
def set_candles_trading_pair(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("trading_pair") return validation_info.data.get("trading_pair")
return v return v
@@ -84,7 +64,7 @@ class MACDBBV1Controller(DirectionalTradingControllerBase):
def __init__(self, config: MACDBBV1ControllerConfig, *args, **kwargs): def __init__(self, config: MACDBBV1ControllerConfig, *args, **kwargs):
self.config = config 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: if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig( self.config.candles_config = [CandlesConfig(
connector=config.candles_connector, connector=config.candles_connector,

View File

@@ -1,9 +1,9 @@
from typing import List, Optional from typing import List
import pandas_ta as ta # noqa: F401 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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import ( from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
DirectionalTradingControllerBase, DirectionalTradingControllerBase,
@@ -14,23 +14,41 @@ from hummingbot.strategy_v2.controllers.directional_trading_controller_base impo
class SuperTrendConfig(DirectionalTradingControllerConfigBase): class SuperTrendConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "supertrend_v1" controller_name: str = "supertrend_v1"
candles_config: List[CandlesConfig] = [] 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_connector: str = Field(
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: ", )) default=None,
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={
length: int = Field(default=20, client_data=ClientFieldData(prompt=lambda mi: "Enter the supertrend length: ", prompt_on_new=True)) "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",
multiplier: float = Field(default=4.0, client_data=ClientFieldData(prompt=lambda mi: "Enter the supertrend multiplier: ", prompt_on_new=True)) "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_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) @field_validator("candles_connector", mode="before")
def set_candles_connector(cls, v, values): @classmethod
def set_candles_connector(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("connector_name") return validation_info.data.get("connector_name")
return v return v
@validator("candles_trading_pair", pre=True, always=True) @field_validator("candles_trading_pair", mode="before")
def set_candles_trading_pair(cls, v, values): @classmethod
def set_candles_trading_pair(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("trading_pair") return validation_info.data.get("trading_pair")
return v return v

View File

@@ -1,55 +1,56 @@
from decimal import Decimal from decimal import Decimal
from typing import Dict, List, Optional, Set 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.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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers import ControllerBase, ControllerConfigBase from hummingbot.strategy_v2.controllers import ControllerBase, ControllerConfigBase
from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, TripleBarrierConfig from hummingbot.strategy_v2.executors.data_types import ConnectorPair
from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction 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.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): class GridStrikeConfig(ControllerConfigBase):
""" """
Configuration required to run the GridStrike strategy for one connector and trading pair. Configuration required to run the GridStrike strategy for one connector and trading pair.
""" """
controller_type: str = "generic"
controller_name: str = "grid_strike" controller_name: str = "grid_strike"
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
controller_type = "generic"
connector_name: str = "binance" # Account configuration
trading_pair: str = "BTC-USDT" leverage: int = 20
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))
position_mode: PositionMode = PositionMode.HEDGE position_mode: PositionMode = PositionMode.HEDGE
leverage: int = 1
time_limit: Optional[int] = Field(default=60 * 60 * 24 * 2, client_data=ClientFieldData(is_updatable=True)) # Boundaries
activation_bounds: Decimal = Field(default=Decimal("0.01"), client_data=ClientFieldData(is_updatable=True)) connector_name: str = "binance_perpetual"
min_spread_between_orders: Optional[Decimal] = Field(default=None, trading_pair: str = "WLD-USDT"
client_data=ClientFieldData(is_updatable=True)) side: TradeType = TradeType.BUY
min_order_amount: Optional[Decimal] = Field(default=Decimal("1"), start_price: Decimal = Field(default=Decimal("0.58"), json_schema_extra={"is_updatable": True})
client_data=ClientFieldData(is_updatable=True)) end_price: Decimal = Field(default=Decimal("0.95"), json_schema_extra={"is_updatable": True})
max_open_orders: int = Field(default=5, client_data=ClientFieldData(is_updatable=True)) limit_price: Decimal = Field(default=Decimal("0.55"), json_schema_extra={"is_updatable": True})
grid_range_update_interval: int = Field(default=60, client_data=ClientFieldData(is_updatable=True))
extra_balance_base_usd: Decimal = Decimal("10") # 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]]: def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]:
if self.connector_name not in markets: if self.connector_name not in markets:
@@ -58,16 +59,6 @@ class GridStrikeConfig(ControllerConfigBase):
return markets 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): class GridStrike(ControllerBase):
def __init__(self, config: GridStrikeConfig, *args, **kwargs): def __init__(self, config: GridStrikeConfig, *args, **kwargs):
super().__init__(config, *args, **kwargs) super().__init__(config, *args, **kwargs)
@@ -75,151 +66,134 @@ class GridStrike(ControllerBase):
self._last_grid_levels_update = 0 self._last_grid_levels_update = 0
self.trading_rules = None self.trading_rules = None
self.grid_levels = [] self.grid_levels = []
self.initialize_rate_sources()
def _calculate_grid_config(self): def initialize_rate_sources(self):
self.trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name, self.market_data_provider.initialize_rate_sources([ConnectorPair(connector_name=self.config.connector_name,
self.config.trading_pair) trading_pair=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 get_balance_requirements(self) -> List[TokenAmount]: def active_executors(self) -> List[ExecutorInfo]:
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]:
return [ return [
executor for executor in self.executors_info 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]: def determine_executor_actions(self) -> List[ExecutorAction]:
if self.market_data_provider.time() - self._last_grid_levels_update > 60: mid_price = self.market_data_provider.get_price_by_type(
self._last_grid_levels_update = self.market_data_provider.time() self.config.connector_name, self.config.trading_pair, PriceType.MidPrice)
self.grid_levels = self._calculate_grid_config() if len(self.active_executors()) == 0 and self.is_inside_bounds(mid_price):
return self.determine_create_executor_actions() + self.determine_stop_executor_actions() 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): async def update_processed_data(self):
mid_price = self.get_mid_price() pass
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),
})
def determine_create_executor_actions(self) -> List[ExecutorAction]: def to_format_status(self) -> List[str]:
mid_price = self.processed_data["mid_price"] status = []
long_activation_bounds = self.processed_data["long_activation_bounds"] mid_price = self.market_data_provider.get_price_by_type(
short_activation_bounds = self.processed_data["short_activation_bounds"] self.config.connector_name, self.config.trading_pair, PriceType.MidPrice)
levels_allowed = [] # Define standard box width for consistency
for level in self.grid_levels: box_width = 114
if (level.side == TradeType.BUY and level.price >= long_activation_bounds) or \ # Top Grid Configuration box with simple borders
(level.side == TradeType.SELL and level.price <= short_activation_bounds): status.append("" + "" * box_width + "")
levels_allowed.append(level) # First line: Grid Configuration and Mid Price
active_executors = self.processed_data["active_executors_order_placed"] + \ left_section = "Grid Configuration:"
self.processed_data["active_executors_order_trading"] padding = box_width - len(left_section) - 4 # -4 for the border characters and spacing
active_executors_level_id = [executor.custom_info["level_id"] for executor in active_executors] config_line1 = f"{left_section}{' ' * padding}"
levels_allowed = sorted([level for level in levels_allowed if level.id not in active_executors_level_id], padding2 = box_width - len(config_line1) + 1 # +1 for correct right border alignment
key=lambda level: abs(level.price - mid_price)) config_line1 += " " * padding2 + ""
levels_allowed = levels_allowed[:self.config.max_open_orders] status.append(config_line1)
create_actions = [] # Second line: Configuration parameters
for level in levels_allowed: 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}"
if level.side == TradeType.BUY and level.price > mid_price: padding = box_width - len(config_line2) + 1 # +1 for correct right border alignment
entry_price = mid_price config_line2 += " " * padding + ""
take_profit = max(level.step * 2, ((level.price - mid_price) / mid_price) + level.step) status.append(config_line2)
trailing_stop = None # Third line: Max orders and Inside bounds
# trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) config_line3 = f"│ Max Orders: {self.config.max_open_orders} │ Inside bounds: {1 if self.is_inside_bounds(mid_price) else 0}"
# trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2) padding = box_width - len(config_line3) + 1 # +1 for correct right border alignment
elif level.side == TradeType.SELL and level.price < mid_price: config_line3 += " " * padding + ""
entry_price = mid_price status.append(config_line3)
take_profit = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) status.append("" + "" * box_width + "")
# trailing_stop_ap = max(level.step * 2, ((mid_price - level.price) / mid_price) + level.step) for level in self.active_executors():
# trailing_stop = TrailingStop(activation_price=trailing_stop_ap, trailing_delta=level.step / 2) # Define column widths for perfect alignment
trailing_stop = None col_width = box_width // 3 # Dividing the total width by 3 for equal columns
else: total_width = box_width
entry_price = level.price # Grid Status header - use long line and running status
take_profit = level.step status_header = f"Grid Status: {level.id} (RunnableStatus.RUNNING)"
trailing_stop = None status_line = f"{status_header}" + "" * (total_width - len(status_header) - 2) + ""
create_actions.append(CreateExecutorAction(controller_id=self.config.id, status.append(status_line)
executor_config=PositionExecutorConfig( # Calculate exact column widths for perfect alignment
timestamp=self.market_data_provider.time(), col1_end = col_width
connector_name=self.config.connector_name, # Column headers
trading_pair=self.config.trading_pair, header_line = "│ Level Distribution" + " " * (col1_end - 20) + ""
entry_price=entry_price, header_line += " Order Statistics" + " " * (col_width - 18) + ""
amount=level.amount, header_line += " Performance Metrics" + " " * (col_width - 21) + ""
leverage=self.config.leverage, status.append(header_line)
side=level.side, # Data for the three columns
level_id=level.id, level_dist_data = [
activation_bounds=[self.config.activation_bounds, f"NOT_ACTIVE: {len(level.custom_info['levels_by_state'].get('NOT_ACTIVE', []))}",
self.config.activation_bounds], f"OPEN_ORDER_PLACED: {len(level.custom_info['levels_by_state'].get('OPEN_ORDER_PLACED', []))}",
triple_barrier_config=TripleBarrierConfig( f"OPEN_ORDER_FILLED: {len(level.custom_info['levels_by_state'].get('OPEN_ORDER_FILLED', []))}",
take_profit=take_profit, f"CLOSE_ORDER_PLACED: {len(level.custom_info['levels_by_state'].get('CLOSE_ORDER_PLACED', []))}",
time_limit=self.config.time_limit, f"COMPLETE: {len(level.custom_info['levels_by_state'].get('COMPLETE', []))}"
open_order_type=OrderType.LIMIT_MAKER, ]
take_profit_order_type=level.take_profit_order_type, order_stats_data = [
trailing_stop=trailing_stop, 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'])}",
return create_actions f"Failed: {len(level.custom_info['failed_orders'])}",
f"Canceled: {len(level.custom_info['canceled_orders'])}"
def determine_stop_executor_actions(self) -> List[ExecutorAction]: ]
long_activation_bounds = self.processed_data["long_activation_bounds"] perf_metrics_data = [
short_activation_bounds = self.processed_data["short_activation_bounds"] f"Buy Vol: {level.custom_info['realized_buy_size_quote']:.4f}",
active_executors_order_placed = self.processed_data["active_executors_order_placed"] f"Sell Vol: {level.custom_info['realized_sell_size_quote']:.4f}",
non_active_ranges = [grid_range.id for grid_range in self.config.grid_ranges if not grid_range.active] f"R. PnL: {level.custom_info['realized_pnl_quote']:.4f}",
active_executor_of_non_active_ranges = [executor.id for executor in self.executors_info if f"R. Fees: {level.custom_info['realized_fees_quote']:.4f}",
executor.is_active and f"P. PnL: {level.custom_info['position_pnl_quote']:.4f}",
executor.custom_info["level_id"].split("_")[0] in non_active_ranges] f"Position: {level.custom_info['position_size_quote']:.4f}"
long_executors_to_stop = [executor.id for executor in active_executors_order_placed if ]
executor.side == TradeType.BUY and # Build rows with perfect alignment
executor.config.entry_price <= long_activation_bounds] max_rows = max(len(level_dist_data), len(order_stats_data), len(perf_metrics_data))
short_executors_to_stop = [executor.id for executor in active_executors_order_placed if for i in range(max_rows):
executor.side == TradeType.SELL and col1 = level_dist_data[i] if i < len(level_dist_data) else ""
executor.config.entry_price >= short_activation_bounds] col2 = order_stats_data[i] if i < len(order_stats_data) else ""
executors_id_to_stop = set( col3 = perf_metrics_data[i] if i < len(perf_metrics_data) else ""
active_executor_of_non_active_ranges + long_executors_to_stop + short_executors_to_stop) row = "" + col1
return [StopExecutorAction(controller_id=self.config.id, executor_id=executor) for executor in row += " " * (col1_end - len(col1) - 2) # -2 for the "│ " at the start
list(executors_id_to_stop)] 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

View File

@@ -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']}"]

View File

@@ -3,9 +3,8 @@ from decimal import Decimal
from typing import Dict, List, Set from typing import Dict, List, Set
import pandas as pd 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.client.ui.interface_utils import format_df_for_printout
from hummingbot.core.data_type.common import PriceType, TradeType from hummingbot.core.data_type.common import PriceType, TradeType
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
@@ -19,62 +18,40 @@ class XEMMMultipleLevelsConfig(ControllerConfigBase):
controller_name: str = "xemm_multiple_levels" controller_name: str = "xemm_multiple_levels"
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
maker_connector: str = Field( maker_connector: str = Field(
default="kucoin", default="mexc",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the maker connector: ", "prompt_on_new": True})
prompt=lambda e: "Enter the maker connector: ",
prompt_on_new=True
))
maker_trading_pair: str = Field( maker_trading_pair: str = Field(
default="LBR-USDT", default="PEPE-USDT",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the maker trading pair: ", "prompt_on_new": True})
prompt=lambda e: "Enter the maker trading pair: ",
prompt_on_new=True
))
taker_connector: str = Field( taker_connector: str = Field(
default="okx", default="binance",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the taker connector: ", "prompt_on_new": True})
prompt=lambda e: "Enter the taker connector: ",
prompt_on_new=True
))
taker_trading_pair: str = Field( taker_trading_pair: str = Field(
default="LBR-USDT", default="PEPE-USDT",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the taker trading pair: ", "prompt_on_new": True})
prompt=lambda e: "Enter the taker trading pair: ",
prompt_on_new=True
))
buy_levels_targets_amount: List[List[Decimal]] = Field( buy_levels_targets_amount: List[List[Decimal]] = Field(
default="0.003,10-0.006,20-0.009,30", default="0.003,10-0.006,20-0.009,30",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda e: "Enter the buy levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", "prompt": "Enter the buy levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ",
prompt_on_new=True "prompt_on_new": True})
))
sell_levels_targets_amount: List[List[Decimal]] = Field( sell_levels_targets_amount: List[List[Decimal]] = Field(
default="0.003,10-0.006,20-0.009,30", default="0.003,10-0.006,20-0.009,30",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda e: "Enter the sell levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ", "prompt": "Enter the sell levels targets with the following structure: (target_profitability1,amount1-target_profitability2,amount2): ",
prompt_on_new=True "prompt_on_new": True})
))
min_profitability: Decimal = Field( min_profitability: Decimal = Field(
default=0.002, default=0.003,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the minimum profitability: ", "prompt_on_new": True})
prompt=lambda e: "Enter the minimum profitability: ",
prompt_on_new=True
))
max_profitability: Decimal = Field( max_profitability: Decimal = Field(
default=0.01, default=0.01,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the maximum profitability: ", "prompt_on_new": True})
prompt=lambda e: "Enter the maximum profitability: ",
prompt_on_new=True
))
max_executors_imbalance: int = Field( max_executors_imbalance: int = Field(
default=1, default=1,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the maximum executors imbalance: ", "prompt_on_new": True})
prompt=lambda e: "Enter the maximum executors imbalance: ",
prompt_on_new=True
))
@validator("buy_levels_targets_amount", "sell_levels_targets_amount", pre=True, always=True) @field_validator("buy_levels_targets_amount", "sell_levels_targets_amount", mode="before")
def validate_levels_targets_amount(cls, v, values): @classmethod
def validate_levels_targets_amount(cls, v):
if isinstance(v, str): if isinstance(v, str):
v = [list(map(Decimal, x.split(","))) for x in v.split("-")] v = [list(map(Decimal, x.split(","))) for x in v.split("-")]
return v 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] 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: 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( config = XEMMExecutorConfig(
controller_id=self.config.id, controller_id=self.config.id,
timestamp=self.market_data_provider.time(), timestamp=self.market_data_provider.time(),
@@ -133,14 +112,16 @@ class XEMMMultipleLevels(ControllerBase):
trading_pair=self.config.taker_trading_pair), trading_pair=self.config.taker_trading_pair),
maker_side=TradeType.BUY, maker_side=TradeType.BUY,
order_amount=amount / mid_price, order_amount=amount / mid_price,
min_profitability=self.config.min_profitability, min_profitability=min_profitability,
target_profitability=target_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)) executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id))
for target_profitability, amount in self.sell_levels_targets_amount: 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] 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: 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( config = XEMMExecutorConfig(
controller_id=self.config.id, controller_id=self.config.id,
timestamp=time.time(), timestamp=time.time(),
@@ -150,9 +131,9 @@ class XEMMMultipleLevels(ControllerBase):
trading_pair=self.config.maker_trading_pair), trading_pair=self.config.maker_trading_pair),
maker_side=TradeType.SELL, maker_side=TradeType.SELL,
order_amount=amount / mid_price, order_amount=amount / mid_price,
min_profitability=self.config.min_profitability, min_profitability=min_profitability,
target_profitability=target_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)) executor_actions.append(CreateExecutorAction(executor_config=config, controller_id=self.config.id))
return executor_actions return executor_actions

View File

@@ -2,9 +2,8 @@ from decimal import Decimal
from typing import List, Optional from typing import List, Optional
import pandas_ta as ta # noqa: F401 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.core.data_type.common import TradeType
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.market_making_controller_base import ( from hummingbot.strategy_v2.controllers.market_making_controller_base import (
@@ -25,38 +24,15 @@ class DManMakerV2Config(MarketMakingControllerConfigBase):
# DCA configuration # DCA configuration
dca_spreads: List[Decimal] = Field( dca_spreads: List[Decimal] = Field(
default="0.01,0.02,0.04,0.08", default="0.01,0.02,0.04,0.08",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter a comma-separated list of spreads for each DCA level: ", "prompt_on_new": True})
prompt_on_new=True,
prompt=lambda mi: "Enter a comma-separated list of spreads for each DCA level: "))
dca_amounts: List[Decimal] = Field( dca_amounts: List[Decimal] = Field(
default="0.1,0.2,0.4,0.8", default="0.1,0.2,0.4,0.8",
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter a comma-separated list of amounts for each DCA level: ", "prompt_on_new": True})
prompt_on_new=True, top_executor_refresh_time: Optional[float] = Field(default=None, json_schema_extra={"is_updatable": True})
prompt=lambda mi: "Enter a comma-separated list of amounts for each DCA level: ")) executor_activation_bounds: Optional[List[Decimal]] = Field(default=None, json_schema_extra={"is_updatable": True})
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) @field_validator("executor_activation_bounds", mode="before")
@classmethod
def parse_activation_bounds(cls, v): def parse_activation_bounds(cls, v):
if isinstance(v, list): if isinstance(v, list):
return [Decimal(val) for val in v] return [Decimal(val) for val in v]
@@ -66,8 +42,9 @@ class DManMakerV2Config(MarketMakingControllerConfigBase):
return [Decimal(val) for val in v.split(",")] return [Decimal(val) for val in v.split(",")]
return v return v
@validator('dca_spreads', pre=True, always=True) @field_validator('dca_spreads', mode="before")
def parse_spreads(cls, v): @classmethod
def parse_dca_spreads(cls, v):
if v is None: if v is None:
return [] return []
if isinstance(v, str): if isinstance(v, str):
@@ -76,15 +53,16 @@ class DManMakerV2Config(MarketMakingControllerConfigBase):
return [float(x.strip()) for x in v.split(',')] return [float(x.strip()) for x in v.split(',')]
return v return v
@validator('dca_amounts', pre=True, always=True) @field_validator('dca_amounts', mode="before")
def parse_and_validate_amounts(cls, v, values, field): @classmethod
def parse_and_validate_dca_amounts(cls, v, validation_info):
if v is None or v == "": 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): if isinstance(v, str):
return [float(x.strip()) for x in v.split(',')] 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( 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 return v
@@ -98,11 +76,11 @@ class DManMakerV2(MarketMakingControllerBase):
def first_level_refresh_condition(self, executor): def first_level_refresh_condition(self, executor):
if self.config.top_executor_refresh_time is not None: if self.config.top_executor_refresh_time is not None:
if self.get_level_from_level_id(executor.custom_info["level_id"]) == 0: 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 return False
def order_level_refresh_condition(self, executor): 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]: def executors_to_refresh(self) -> List[ExecutorAction]:
executors_to_refresh = self.filter_executors( executors_to_refresh = self.filter_executors(

View File

@@ -2,9 +2,9 @@ from decimal import Decimal
from typing import List from typing import List
import pandas_ta as ta # noqa: F401 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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.market_making_controller_base import ( from hummingbot.strategy_v2.controllers.market_making_controller_base import (
MarketMakingControllerBase, MarketMakingControllerBase,
@@ -14,69 +14,60 @@ from hummingbot.strategy_v2.executors.position_executor.data_types import Positi
class PMMDynamicControllerConfig(MarketMakingControllerConfigBase): class PMMDynamicControllerConfig(MarketMakingControllerConfigBase):
controller_name = "pmm_dynamic" controller_name: str = "pmm_dynamic"
candles_config: List[CandlesConfig] = [] candles_config: List[CandlesConfig] = []
buy_spreads: List[float] = Field( buy_spreads: List[float] = Field(
default="1,2,4", default="1,2,4",
client_data=ClientFieldData( json_schema_extra={
is_updatable=True, "prompt": "Enter a comma-separated list of buy spreads measured in units of volatility(e.g., '1, 2'): ",
prompt_on_new=True, "prompt_on_new": True, "is_updatable": True}
prompt=lambda mi: "Enter a comma-separated list of buy spreads (e.g., '0.01, 0.02'):")) )
sell_spreads: List[float] = Field( sell_spreads: List[float] = Field(
default="1,2,4", default="1,2,4",
client_data=ClientFieldData( json_schema_extra={
is_updatable=True, "prompt": "Enter a comma-separated list of sell spreads measured in units of volatility(e.g., '1, 2'): ",
prompt_on_new=True, "prompt_on_new": True, "is_updatable": True}
prompt=lambda mi: "Enter a comma-separated list of sell spreads (e.g., '0.01, 0.02'):")) )
candles_connector: str = Field( candles_connector: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ",
prompt=lambda mi: "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( candles_trading_pair: str = Field(
default=None, default=None,
client_data=ClientFieldData( json_schema_extra={
prompt_on_new=True, "prompt": "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ",
prompt=lambda mi: "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( interval: str = Field(
default="3m", default="3m",
client_data=ClientFieldData( json_schema_extra={
prompt=lambda mi: "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ", "prompt": "Enter the candle interval (e.g., 1m, 5m, 1h, 1d): ",
prompt_on_new=False)) "prompt_on_new": True})
macd_fast: int = Field( macd_fast: int = Field(
default=12, default=21,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD fast period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD fast length: ",
prompt_on_new=True))
macd_slow: int = Field( macd_slow: int = Field(
default=26, default=42,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD slow period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD slow length: ",
prompt_on_new=True))
macd_signal: int = Field( macd_signal: int = Field(
default=9, default=9,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the MACD signal period: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the MACD signal length: ",
prompt_on_new=True))
natr_length: int = Field( natr_length: int = Field(
default=14, default=14,
client_data=ClientFieldData( json_schema_extra={"prompt": "Enter the NATR length: ", "prompt_on_new": True})
prompt=lambda mi: "Enter the NATR length: ",
prompt_on_new=True))
@validator("candles_connector", pre=True, always=True) @field_validator("candles_connector", mode="before")
def set_candles_connector(cls, v, values): @classmethod
def set_candles_connector(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("connector_name") return validation_info.data.get("connector_name")
return v return v
@validator("candles_trading_pair", pre=True, always=True) @field_validator("candles_trading_pair", mode="before")
def set_candles_trading_pair(cls, v, values): @classmethod
def set_candles_trading_pair(cls, v, validation_info: ValidationInfo):
if v is None or v == "": if v is None or v == "":
return values.get("trading_pair") return validation_info.data.get("trading_pair")
return v return v
@@ -87,7 +78,7 @@ class PMMDynamicController(MarketMakingControllerBase):
""" """
def __init__(self, config: PMMDynamicControllerConfig, *args, **kwargs): def __init__(self, config: PMMDynamicControllerConfig, *args, **kwargs):
self.config = config 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: if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig( self.config.candles_config = [CandlesConfig(
connector=config.candles_connector, connector=config.candles_connector,

View File

@@ -3,7 +3,6 @@ from typing import List
from pydantic import Field 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.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.market_making_controller_base import ( from hummingbot.strategy_v2.controllers.market_making_controller_base import (
MarketMakingControllerBase, MarketMakingControllerBase,
@@ -13,9 +12,9 @@ from hummingbot.strategy_v2.executors.position_executor.data_types import Positi
class PMMSimpleConfig(MarketMakingControllerConfigBase): 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 # 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): class PMMSimpleController(MarketMakingControllerBase):