microsoft/qlib · error · ValueError

atomic executor doesn't support specify `range_limit`

Error message

atomic executor doesn't support specify `range_limit`

What it means

BaseExecutor.execute_folder/check: an atomic executor (one that is not a NestedExecutor subclass) executes decisions within a single step and cannot honor a range_limit spanning multiple steps. If a decision passed to an atomic executor reports a non-None get_range_limit(default_value=None), the mismatch is a programming error and ValueError is raised.

Source

Thrown at qlib/backtest/executor.py:268

        execute_result : List[object]
            the executed result for trade decision.
            ** NOTE!!!! **:
            1) This is necessary,  The return value of generator will be used in NestedExecutor
            2) Please note the executed results are not merged.

        Yields
        -------
        object
            trade decision
        """

        if self.track_data:
            yield trade_decision

        atomic = not issubclass(self.__class__, NestedExecutor)  # issubclass(A, A) is True

        if atomic and trade_decision.get_range_limit(default_value=None) is not None:
            raise ValueError("atomic executor doesn't support specify `range_limit`")

        if self._settle_type != BasePosition.ST_NO:
            self.trade_account.current_position.settle_start(self._settle_type)

        obj = self._collect_data(trade_decision=trade_decision, level=level)

        if isinstance(obj, GeneratorType):
            yield_res = yield from obj
            assert isinstance(yield_res, tuple) and len(yield_res) == 2
            res, kwargs = yield_res
        else:
            # Some concrete executor don't have inner decisions
            res, kwargs = obj

        trade_start_time, trade_end_time = self.trade_calendar.get_step_time()
        # Account will not be changed in this function
        self.trade_account.update_bar_end(
            trade_start_time,

View on GitHub (pinned to 79633dd950)

Solutions

  1. Ensure decisions with a trade_range are executed by NestedExecutor levels, with only range-limit-free decisions reaching atomic executors
  2. If your custom executor supports multi-step execution, inherit from NestedExecutor instead of BaseExecutor
  3. Strip trade_range (set decision.trade_range = None) before handing the decision to an atomic executor if the whole-step semantics are acceptable

Example fix

# before
# SimulatorExecutor (atomic) receives decision with trade_range -> ValueError
res = sim_executor.run(execution_strategy, level=0)
# after
# route through a NestedExecutor for the outer level
res = nested_executor.run(execution_strategy, level=0)
Defensive patterns

Strategy: validation

Validate before calling

from qlib.backtest.executor import NestedExecutor
atomic = not isinstance(executor, NestedExecutor)
if atomic and decision.get_range_limit(default_value=None) is not None:
    decision.trade_range = None  # or route the decision to a nested executor instead
executor.run(...)  # proceed

Type guard

from qlib.backtest.executor import NestedExecutor
def is_atomic(executor) -> bool:
    return not isinstance(executor, NestedExecutor)

Prevention

When it happens

Trigger: Registering SimulatorExecutor (atomic) as the innermost executor while the strategy's decisions carry a trade_range/range_limit; configuring an executor stack where a decision with a range limit reaches a non-nested executor; subclassing BaseExecutor without inheriting NestedExecutor.

Common situations: Custom executor classes deriving from BaseExecutor directly and fed decisions from NestedExecutor-style strategies; mismatched executor/strategy configs in backtest yaml.

Related errors


AI-assisted analysis of microsoft/qlib@79633dd950 (2026-08-15). Data as JSON: /api/errors/5cd72d29b1796728. Report an issue: GitHub.