Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ repos:
args: [--settings-path=pyproject.toml]

- repo: https://github.com/pycqa/flake8
rev: 3.9.2
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: ['flake8']
args: [--max-line-length=130]
84 changes: 84 additions & 0 deletions bots/controllers/directional_trading/ai_livestream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from decimal import Decimal
from typing import List

import pandas_ta as ta # noqa: F401
from pydantic import Field

from hummingbot.core.data_type.common import TradeType
from hummingbot.remote_iface.mqtt import ExternalTopicFactory
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
DirectionalTradingControllerBase,
DirectionalTradingControllerConfigBase,
)
from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig


class AILivestreamControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "ai_livestream"
long_threshold: float = Field(default=0.5, json_schema_extra={"is_updatable": True})
short_threshold: float = Field(default=0.5, json_schema_extra={"is_updatable": True})
topic: str = "hbot/predictions"


class AILivestreamController(DirectionalTradingControllerBase):
def __init__(self, config: AILivestreamControllerConfig, *args, **kwargs):
self.config = config
super().__init__(config, *args, **kwargs)
# Start ML signal listener
self._init_ml_signal_listener()

def _init_ml_signal_listener(self):
"""Initialize a listener for ML signals from the MQTT broker"""
try:
normalized_pair = self.config.trading_pair.replace("-", "_").lower()
topic = f"{self.config.topic}/{normalized_pair}/ML_SIGNALS"
self._ml_signal_listener = ExternalTopicFactory.create_async(
topic=topic,
callback=self._handle_ml_signal,
use_bot_prefix=False,
)
self.logger().info("ML signal listener initialized successfully")
except Exception as e:
self.logger().error(f"Failed to initialize ML signal listener: {str(e)}")
self._ml_signal_listener = None

def _handle_ml_signal(self, signal: dict, topic: str):
"""Handle incoming ML signal"""
# self.logger().info(f"Received ML signal: {signal}")
short, neutral, long = signal["probabilities"]
if short > self.config.short_threshold:
self.processed_data["signal"] = -1
elif long > self.config.long_threshold:
self.processed_data["signal"] = 1
else:
self.processed_data["signal"] = 0
self.processed_data["features"] = signal

async def update_processed_data(self):
pass

def get_executor_config(self, trade_type: TradeType, price: Decimal, amount: Decimal):
"""
Get the executor config based on the trade_type, price and amount. This method can be overridden by the
subclasses if required.
"""
return PositionExecutorConfig(
timestamp=self.market_data_provider.time(),
connector_name=self.config.connector_name,
trading_pair=self.config.trading_pair,
side=trade_type,
entry_price=price,
amount=amount,
triple_barrier_config=self.config.triple_barrier_config.new_instance_with_adjusted_volatility(
volatility_factor=self.processed_data["features"].get("target_pct", 0.01)),
leverage=self.config.leverage,
)

def to_format_status(self) -> List[str]:
lines = []
features = self.processed_data.get("features", {})
lines.append(f"Signal: {self.processed_data.get('signal', 'N/A')}")
lines.append(f"Timestamp: {features.get('timestamp', 'N/A')}")
lines.append(f"Probabilities: {features.get('probabilities', 'N/A')}")
lines.append(f"Target Pct: {features.get('target_pct', 'N/A')}")
return lines
20 changes: 10 additions & 10 deletions bots/controllers/directional_trading/bollinger_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

class BollingerV1ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "bollinger_v1"
candles_config: List[CandlesConfig] = []
candles_connector: str = Field(
default=None,
json_schema_extra={
Expand Down Expand Up @@ -55,23 +54,24 @@ class BollingerV1Controller(DirectionalTradingControllerBase):
def __init__(self, config: BollingerV1ControllerConfig, *args, **kwargs):
self.config = config
self.max_records = self.config.bb_length
if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig(
connector=config.candles_connector,
trading_pair=config.candles_trading_pair,
interval=config.interval,
max_records=self.max_records
)]
super().__init__(config, *args, **kwargs)

def get_candles_config(self) -> List[CandlesConfig]:
return [CandlesConfig(
connector=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records
)]

async def update_processed_data(self):
df = self.market_data_provider.get_candles_df(connector_name=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records)
# Add indicators
df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"]
df.ta.bbands(length=self.config.bb_length, lower_std=self.config.bb_std, upper_std=self.config.bb_std, append=True)
bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}_{self.config.bb_std}"]

# Generate signal
long_condition = bbp < self.config.bb_long_threshold
Expand Down
118 changes: 118 additions & 0 deletions bots/controllers/directional_trading/bollinger_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from sys import float_info as sflt
from typing import List

import pandas as pd
import pandas_ta as ta # noqa: F401
import talib
from pydantic import Field, field_validator
from pydantic_core.core_schema import ValidationInfo
from talib import MA_Type

from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
DirectionalTradingControllerBase,
DirectionalTradingControllerConfigBase,
)


class BollingerV2ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "bollinger_v2"
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})
bb_length: int = Field(
default=100,
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)

@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


class BollingerV2Controller(DirectionalTradingControllerBase):
def __init__(self, config: BollingerV2ControllerConfig, *args, **kwargs):
self.config = config
self.max_records = self.config.bb_length * 5
super().__init__(config, *args, **kwargs)

def get_candles_config(self) -> List[CandlesConfig]:
return [CandlesConfig(
connector=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records
)]

def non_zero_range(self, x: pd.Series, y: pd.Series) -> pd.Series:
"""Non-Zero Range

Calculates the difference of two Series plus epsilon to any zero values.
Technically: ```x - y + epsilon```

Parameters:
x (Series): Series of 'x's
y (Series): Series of 'y's

Returns:
(Series): 1 column
"""
diff = x - y
if diff.eq(0).any().any():
diff += sflt.epsilon
return diff

async def update_processed_data(self):
df = self.market_data_provider.get_candles_df(connector_name=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records)
# Add indicators
df.ta.bbands(length=self.config.bb_length, lower_std=self.config.bb_std, upper_std=self.config.bb_std, append=True)
df["upperband"], df["middleband"], df["lowerband"] = talib.BBANDS(real=df["close"], timeperiod=self.config.bb_length, nbdevup=self.config.bb_std, nbdevdn=self.config.bb_std, matype=MA_Type.SMA)

ulr = self.non_zero_range(df["upperband"], df["lowerband"])
bbp = self.non_zero_range(df["close"], df["lowerband"]) / ulr
df["percent"] = bbp

# Generate signal
long_condition = bbp < self.config.bb_long_threshold
short_condition = bbp > self.config.bb_short_threshold

# Generate signal
df["signal"] = 0
df.loc[long_condition, "signal"] = 1
df.loc[short_condition, "signal"] = -1

# Debug
# We skip the last row which is live candle
with pd.option_context('display.max_rows', None, 'display.max_columns', None, 'display.width', None):
self.logger().info(df.head(-1).tail(15))

# Update processed data
self.processed_data["signal"] = df["signal"].iloc[-1]
self.processed_data["features"] = df
16 changes: 8 additions & 8 deletions bots/controllers/directional_trading/bollingrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

class BollinGridControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "bollingrid"
candles_config: List[CandlesConfig] = []
candles_connector: str = Field(
default=None,
json_schema_extra={
Expand Down Expand Up @@ -84,13 +83,6 @@ class BollinGridController(DirectionalTradingControllerBase):
def __init__(self, config: BollinGridControllerConfig, *args, **kwargs):
self.config = config
self.max_records = self.config.bb_length
if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig(
connector=config.candles_connector,
trading_pair=config.candles_trading_pair,
interval=config.interval,
max_records=self.max_records
)]
super().__init__(config, *args, **kwargs)

async def update_processed_data(self):
Expand Down Expand Up @@ -158,3 +150,11 @@ def get_executor_config(self, trade_type: TradeType, price: Decimal, amount: Dec
min_order_amount_quote=self.config.min_order_amount_quote,
max_open_orders=self.config.max_open_orders,
)

def get_candles_config(self) -> List[CandlesConfig]:
return [CandlesConfig(
connector=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records
)]
25 changes: 13 additions & 12 deletions bots/controllers/directional_trading/dman_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

class DManV3ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "dman_v3"
candles_config: List[CandlesConfig] = []
candles_connector: str = Field(
default=None,
json_schema_extra={
Expand Down Expand Up @@ -143,16 +142,10 @@ class DManV3Controller(DirectionalTradingControllerBase):
Mean reversion strategy with Grid execution making use of Bollinger Bands indicator to make spreads dynamic
and shift the mid-price.
"""

def __init__(self, config: DManV3ControllerConfig, *args, **kwargs):
self.config = config
self.max_records = config.bb_length
if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig(
connector=config.candles_connector,
trading_pair=config.candles_trading_pair,
interval=config.interval,
max_records=self.max_records
)]
super().__init__(config, *args, **kwargs)

async def update_processed_data(self):
Expand All @@ -161,11 +154,11 @@ async def update_processed_data(self):
interval=self.config.interval,
max_records=self.max_records)
# Add indicators
df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
df.ta.bbands(length=self.config.bb_length, lower_std=self.config.bb_std, upper_std=self.config.bb_std, append=True)

# Generate signal
long_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"] < self.config.bb_long_threshold
short_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"] > self.config.bb_short_threshold
long_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}_{self.config.bb_std}"] < self.config.bb_long_threshold
short_condition = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}_{self.config.bb_std}"] > self.config.bb_short_threshold

# Generate signal
df["signal"] = 0
Expand All @@ -179,7 +172,7 @@ async def update_processed_data(self):
def get_spread_multiplier(self) -> Decimal:
if self.config.dynamic_order_spread:
df = self.processed_data["features"]
bb_width = df[f"BBB_{self.config.bb_length}_{self.config.bb_std}"].iloc[-1]
bb_width = df[f"BBB_{self.config.bb_length}_{self.config.bb_std}_{self.config.bb_std}"].iloc[-1]
return Decimal(bb_width / 200)
else:
return Decimal("1.0")
Expand Down Expand Up @@ -216,3 +209,11 @@ def get_executor_config(self, trade_type: TradeType, price: Decimal, amount: Dec
leverage=self.config.leverage,
activation_bounds=self.config.activation_bounds,
)

def get_candles_config(self) -> List[CandlesConfig]:
return [CandlesConfig(
connector=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records
)]
20 changes: 10 additions & 10 deletions bots/controllers/directional_trading/macd_bb_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

class MACDBBV1ControllerConfig(DirectionalTradingControllerConfigBase):
controller_name: str = "macd_bb_v1"
candles_config: List[CandlesConfig] = []
candles_connector: str = Field(
default=None,
json_schema_extra={
Expand Down Expand Up @@ -65,13 +64,6 @@ 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) + 20
if len(self.config.candles_config) == 0:
self.config.candles_config = [CandlesConfig(
connector=config.candles_connector,
trading_pair=config.candles_trading_pair,
interval=config.interval,
max_records=self.max_records
)]
super().__init__(config, *args, **kwargs)

async def update_processed_data(self):
Expand All @@ -80,10 +72,10 @@ async def update_processed_data(self):
interval=self.config.interval,
max_records=self.max_records)
# Add indicators
df.ta.bbands(length=self.config.bb_length, std=self.config.bb_std, append=True)
df.ta.bbands(length=self.config.bb_length, lower_std=self.config.bb_std, upper_std=self.config.bb_std, append=True)
df.ta.macd(fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal, append=True)

bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}"]
bbp = df[f"BBP_{self.config.bb_length}_{self.config.bb_std}_{self.config.bb_std}"]
macdh = df[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
macd = df[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]

Expand All @@ -98,3 +90,11 @@ async def update_processed_data(self):
# Update processed data
self.processed_data["signal"] = df["signal"].iloc[-1]
self.processed_data["features"] = df

def get_candles_config(self) -> List[CandlesConfig]:
return [CandlesConfig(
connector=self.config.candles_connector,
trading_pair=self.config.candles_trading_pair,
interval=self.config.interval,
max_records=self.max_records
)]
Loading