(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
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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

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
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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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):