Skip to content

load_controller_config_class returns the base config class for any controller whose config name sorts after "D" #198

Description

@fengtality

FileSystemUtil.load_controller_config_class (utils/file_system.py:323) resolves the wrong class for many controllers, silently exposing only the base schema.

Root cause

for _, cls in inspect.getmembers(script_module, inspect.isclass):
    is_directional = (issubclass(cls, DirectionalTradingControllerConfigBase)
                      and cls is not DirectionalTradingControllerConfigBase)
    is_market_making = (issubclass(cls, MarketMakingControllerConfigBase)
                        and cls is not MarketMakingControllerConfigBase)
    is_generic = (issubclass(cls, ControllerConfigBase)
                  and cls is not ControllerConfigBase)
    if is_directional or is_market_making or is_generic:
        return cls

is_directional correctly excludes the directional base, but is_generic does not: DirectionalTradingControllerConfigBase is a subclass of ControllerConfigBase and is not ControllerConfigBase, so is_generic evaluates True for the base class itself. The same holds for MarketMakingControllerConfigBase.

Because inspect.getmembers returns members sorted alphabetically and the loop returns on the first match, the base class wins whenever it sorts before the concrete config class. Any controller module that imports its base under the normal name is affected if its config class name sorts after DirectionalTradingControllerConfigBase / MarketMakingControllerConfigBase.

Impact

  • GET /controllers/{type}/{name}/config-schema reports only the ~13 base fields.
  • Config validation (routers/controllers.py:332, config_class(**config)) then rejects every strategy-specific parameter as an unknown field, so saving a valid config for an affected controller is impossible.
  • This affects the shipped supertrend_v1 controller (SuperTrendConfig, S > D). bollinger_v1 works only by alphabetical accident (BollingerV1ControllerConfig, B < D).

Reproduce

GET /controllers/directional_trading/bollinger_v1/config-schema    # full schema  (B < D)  OK
GET /controllers/directional_trading/supertrend_v1/config-schema   # base only    (S > D)  BUG

supertrend_v1 returns just total_amount_quote, manual_kill_switch, connector_name, trading_pair, max_executors_per_side, cooldown_time, leverage, position_mode, stop_loss, take_profit, time_limit, take_profit_order_type, trailing_stop — none of its length, multiplier, percentage_threshold, candles fields.

Isolating it inside the container confirms the resolver has both classes available and picks the wrong one:

import importlib, inspect
m = importlib.import_module('bots.controllers.directional_trading.<name>')
# enumerating ControllerConfigBase subclasses yields:
#   <ConcreteConfig>                          42 fields
#   DirectionalTradingControllerConfigBase    17 fields   <- returned

Suggested fix

Exclude all base classes, not just the type-specific one, and ignore classes that were merely imported into the module:

BASE_CONFIG_CLASSES = (
    ControllerConfigBase,
    DirectionalTradingControllerConfigBase,
    MarketMakingControllerConfigBase,
)

for _, cls in inspect.getmembers(script_module, inspect.isclass):
    if not issubclass(cls, ControllerConfigBase) or cls in BASE_CONFIG_CLASSES:
        continue
    if cls.__module__ != script_module.__name__:   # skip imported classes
        continue
    return cls

The __module__ guard is worth adding regardless — it makes resolution independent of both import style and alphabetical ordering.

Workaround for controller authors

Until this is fixed, importing the bases under underscore-prefixed aliases makes the concrete class sort first (_ = 0x5F sorts after Z = 0x5A):

from hummingbot.strategy_v2.controllers.directional_trading_controller_base import (
    DirectionalTradingControllerBase as _DirectionalTradingControllerBase,
    DirectionalTradingControllerConfigBase as _DirectionalTradingControllerConfigBase,
)

Found while porting a custom directional controller (PullbackKCV2ControllerConfig, P > D) for backtesting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions