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.
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
is_directionalcorrectly excludes the directional base, butis_genericdoes not:DirectionalTradingControllerConfigBaseis a subclass ofControllerConfigBaseand is notControllerConfigBase, sois_genericevaluatesTruefor the base class itself. The same holds forMarketMakingControllerConfigBase.Because
inspect.getmembersreturns members sorted alphabetically and the loopreturns 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 afterDirectionalTradingControllerConfigBase/MarketMakingControllerConfigBase.Impact
GET /controllers/{type}/{name}/config-schemareports only the ~13 base fields.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.supertrend_v1controller (SuperTrendConfig,S>D).bollinger_v1works only by alphabetical accident (BollingerV1ControllerConfig,B<D).Reproduce
supertrend_v1returns justtotal_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 itslength,multiplier,percentage_threshold, candles fields.Isolating it inside the container confirms the resolver has both classes available and picks the wrong one:
Suggested fix
Exclude all base classes, not just the type-specific one, and ignore classes that were merely imported into the module:
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 afterZ= 0x5A):Found while porting a custom directional controller (
PullbackKCV2ControllerConfig,P>D) for backtesting.