Skip to content

Commit

Permalink
Merge pull request hummingbot#6915 from cardosofede/feat/twap_executor
Browse files Browse the repository at this point in the history
Feat/twap executor
  • Loading branch information
nikspz authored Mar 25, 2024
2 parents 851044d + e1ed022 commit 0e1e111
Show file tree
Hide file tree
Showing 22 changed files with 902 additions and 30 deletions.
2 changes: 2 additions & 0 deletions controllers/directional_trading/dman_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ class DManV3ControllerConfig(DirectionalTradingControllerConfigBase):
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))
dca_spreads: List[Decimal] = Field(
Expand Down
25 changes: 24 additions & 1 deletion controllers/market_making/pmm_simple.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time
from decimal import Decimal
from typing import List
from typing import List, Optional

from pydantic import Field

Expand All @@ -11,19 +11,42 @@
MarketMakingControllerConfigBase,
)
from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorConfig
from hummingbot.smart_components.models.executor_actions import ExecutorAction, StopExecutorAction


class PMMSimpleConfig(MarketMakingControllerConfigBase):
controller_name = "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))
top_order_refresh_time: Optional[float] = Field(
default=None,
client_data=ClientFieldData(
is_updatable=True,
prompt_on_new=False))


class PMMSimpleController(MarketMakingControllerBase):
def __init__(self, config: PMMSimpleConfig, *args, **kwargs):
super().__init__(config, *args, **kwargs)
self.config = config

def first_level_refresh_condition(self, executor):
if self.config.top_order_refresh_time is not None:
if self.get_level_from_level_id(executor.custom_info["level_id"]) == 1:
return time.time() - executor.timestamp > self.config.top_order_refresh_time
return False

def order_level_refresh_condition(self, executor):
return time.time() - executor.timestamp > self.config.executor_refresh_time

def executors_to_refresh(self) -> List[ExecutorAction]:
executors_to_refresh = self.filter_executors(
executors=self.executors_info,
filter_func=lambda x: not x.is_trading and x.is_active and (self.order_level_refresh_condition(x) or self.first_level_refresh_condition(x)))
return [StopExecutorAction(
controller_id=self.config.id,
executor_id=executor.id) for executor in executors_to_refresh]

def get_executor_config(self, level_id: str, price: Decimal, amount: Decimal):
trade_type = self.get_trade_type_from_level_id(level_id)
return PositionExecutorConfig(
Expand Down
7 changes: 4 additions & 3 deletions hummingbot/client/command/start_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from hummingbot.exceptions import InvalidScriptModule, OracleRateUnavailable
from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase

if TYPE_CHECKING:
from hummingbot.client.hummingbot_application import HummingbotApplication # noqa: F401
Expand Down Expand Up @@ -213,15 +214,15 @@ def load_script_class(self):
try:
script_class = next((member for member_name, member in inspect.getmembers(script_module)
if inspect.isclass(member) and
(issubclass(member, ScriptStrategyBase) or issubclass(member, DirectionalStrategyBase)) and
member not in [ScriptStrategyBase, DirectionalStrategyBase]))
issubclass(member, ScriptStrategyBase) and
member not in [ScriptStrategyBase, DirectionalStrategyBase, StrategyV2Base]))
except StopIteration:
raise InvalidScriptModule(f"The module {script_name} does not contain any subclass of ScriptStrategyBase")
if self.strategy_name != self.strategy_file_name:
try:
config_class = next((member for member_name, member in inspect.getmembers(script_module)
if inspect.isclass(member) and
issubclass(member, BaseClientModel) and member not in [BaseClientModel]))
issubclass(member, BaseClientModel) and member not in [BaseClientModel, StrategyV2ConfigBase]))
config = config_class(**self.load_script_yaml_config(config_file_path=self.strategy_file_name))
script_class.init_markets(config)
except StopIteration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async def _parse_funding_info_message(self, raw_message: Dict[str, Any], message
info_update.mark_price = Decimal(str(entries["markPrice"]))
if "fundingRate" in entries:
info_update.rate = Decimal(str(entries["fundingRate"]))
message_queue.put_nowait(info_update)
message_queue.put_nowait(info_update)

async def _request_complete_funding_info(self, trading_pair: str) -> Dict[str, Any]:
exchange_symbol = await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair),
Expand Down
14 changes: 13 additions & 1 deletion hummingbot/smart_components/controllers/controller_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

from hummingbot.client.config.config_data_types import BaseClientModel, ClientFieldData
from hummingbot.core.event.event_forwarder import SourceInfoEventForwarder
from hummingbot.core.utils.async_utils import safe_ensure_future
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
from hummingbot.data_feed.market_data_provider import MarketDataProvider
from hummingbot.smart_components.models.base import SmartComponentStatus
from hummingbot.smart_components.models.executor_actions import ExecutorAction
from hummingbot.smart_components.models.executors_info import ExecutorInfo
from hummingbot.smart_components.smart_component_base import SmartComponentBase
Expand All @@ -35,6 +37,7 @@ class ControllerConfigBase(BaseClientModel):
))
controller_name: str
controller_type: str = "generic"
manual_kill_switch: bool = Field(default=None, client_data=ClientFieldData(is_updatable=True, prompt_on_new=False))
candles_config: List[CandlesConfig] = Field(
default="binance_perpetual.WLD-USDT.1m.500",
client_data=ClientFieldData(
Expand Down Expand Up @@ -120,7 +123,6 @@ def __init__(self, config: ControllerConfigBase, market_data_provider: MarketDat
self.market_data_provider: MarketDataProvider = market_data_provider
self.actions_queue: asyncio.Queue = actions_queue
self.processed_data = {}
self.initialize_candles()
self.executors_update_event = asyncio.Event()
self.executors_update_listener = SourceInfoEventForwarder(to_function=self.handle_executor_update)

Expand All @@ -133,6 +135,16 @@ def handle_executor_update(self, event_tag, event_caller, executors_info):
self.executors_info = executors_info.get(self.config.id, [])
self.executors_update_event.set()

def start(self):
"""
Allow controllers to be restarted after being stopped.=
"""
if self._status != SmartComponentStatus.RUNNING:
self.terminated.clear()
self._status = SmartComponentStatus.RUNNING
safe_ensure_future(self.control_loop())
self.initialize_candles()

def initialize_candles(self):
for candles_config in self.config.candles_config:
self.market_data_provider.initialize_candles_feed(candles_config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ def triple_barrier_config(self) -> TripleBarrierConfig:

@validator('buy_spreads', 'sell_spreads', pre=True, always=True)
def parse_spreads(cls, v):
if v is None:
return []
if isinstance(v, str):
if v == "":
return []
return [float(x.strip()) for x in v.split(',')]
return v

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from hummingbot.smart_components.executors.dca_executor.dca_executor import DCAExecutor
from hummingbot.smart_components.executors.position_executor.data_types import PositionExecutorConfig
from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
from hummingbot.smart_components.executors.twap_executor.data_types import TWAPExecutorConfig
from hummingbot.smart_components.executors.twap_executor.twap_executor import TWAPExecutor
from hummingbot.smart_components.models.executor_actions import (
CreateExecutorAction,
ExecutorAction,
Expand Down Expand Up @@ -44,7 +46,8 @@ def stop(self):
# first we stop all active executors
for controller_id, executors_list in self.executors.items():
for executor in executors_list:
executor.early_stop()
if not executor.is_closed:
executor.early_stop()
# then we store all executors
for controller_id, executors_list in self.executors.items():
for executor in executors_list:
Expand Down Expand Up @@ -89,6 +92,8 @@ def create_executor(self, action: CreateExecutorAction):
executor = DCAExecutor(self.strategy, executor_config, self.executors_update_interval)
elif isinstance(executor_config, ArbitrageExecutorConfig):
executor = ArbitrageExecutor(self.strategy, executor_config, self.executors_update_interval)
elif isinstance(executor_config, TWAPExecutorConfig):
executor = TWAPExecutor(self.strategy, executor_config, self.executors_update_interval)
else:
raise ValueError("Unsupported executor config type")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,29 @@ async def control_task(self):
await self.control_shutdown_process()
self.evaluate_max_retries()

def open_orders_completed(self):
"""
This method is responsible for checking if the open orders are completed.
:return: True if the open orders are completed, False otherwise.
"""
open_order_condition = not self._open_order or self._open_order.is_done
take_profit_condition = not self._take_profit_limit_order or self._take_profit_limit_order.is_done
failed_orders_condition = not self._failed_orders or all([order.is_done for order in self._failed_orders])
return open_order_condition and take_profit_condition and failed_orders_condition

async def control_shutdown_process(self):
"""
This method is responsible for controlling the shutdown process of the executor.
:return: None
"""
if math.isclose(self.open_filled_amount, self.close_filled_amount):
self.stop()
if self.open_orders_completed():
self.stop()
else:
self.cancel_open_orders()
self._current_retries += 1
elif self._close_order:
self.logger().info(f"Waiting for close order to be filled --> Filled amount: {self.close_filled_amount} | Open amount: {self.open_filled_amount}")
else:
Expand Down Expand Up @@ -399,9 +414,9 @@ def cancel_open_orders(self):
:return: None
"""
if self._open_order:
if self._open_order and self._open_order.order and self._open_order.order.is_open:
self.cancel_open_order()
if self._take_profit_limit_order:
if self._take_profit_limit_order and self._take_profit_limit_order.order and self._take_profit_limit_order.order.is_open:
self.cancel_take_profit()

def control_stop_loss(self):
Expand Down
Empty file.
51 changes: 51 additions & 0 deletions hummingbot/smart_components/executors/twap_executor/data_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from decimal import Decimal
from enum import Enum
from typing import Optional

from pydantic import validator

from hummingbot.core.data_type.common import OrderType, TradeType
from hummingbot.smart_components.executors.data_types import ExecutorConfigBase


class TWAPMode(Enum):
MAKER = "MAKER"
TAKER = "TAKER"


class TWAPExecutorConfig(ExecutorConfigBase):
type: str = "twap_executor"
connector_name: str
trading_pair: str
side: TradeType
leverage: int = 1
total_amount_quote: Decimal
total_duration: int
order_interval: int
mode: TWAPMode = TWAPMode.TAKER

# MAKER mode specific parameters
limit_order_buffer: Optional[Decimal] = None
order_resubmission_time: Optional[int] = None

@validator('limit_order_buffer', pre=True, always=True)
def validate_limit_order_buffer(cls, v, values):
if v is None and values["mode"] == TWAPMode.MAKER:
raise ValueError("limit_order_buffer is required for MAKER mode")
return v

@property
def is_maker(self) -> bool:
return self.mode == TWAPMode.MAKER

@property
def number_of_orders(self) -> int:
return (self.total_duration // self.order_interval) + 1

@property
def order_amount_quote(self) -> Decimal:
return self.total_amount_quote / self.number_of_orders

@property
def order_type(self) -> OrderType:
return OrderType.LIMIT if self.is_maker else OrderType.MARKET
Loading

0 comments on commit 0e1e111

Please sign in to comment.