microsoft/qlib · error · ValueError

inner_order_indicators is necessary in un-atomic executor

Error message

inner_order_indicators is necessary in un-atomic executor

What it means

DiskDatasetCache._dataset (qlib/data/cache.py:706) raises ValueError when inst_processors is non-empty and disk_cache != 0. The on-disk dataset cache stores a raw (resampled) frame keyed by the dataset hash, so per-instrument processors (e.g. price normalization) cannot be baked into the shared cache file without corrupting it for other configs. The error message gives the two supported escapes: D.features(disk_cache=0) or qlib.init(dataset_cache=None).

Source

Thrown at qlib/backtest/account.py:383

            external trade decision
        trade_info : List[(Order, float, float, float)], optional
            trading information, by default None
            - necessary if atomic is True
            - list of tuple(order, trade_val, trade_cost, trade_price)
        inner_order_indicators : Indicator, optional
            indicators of inner executor, by default None
            - necessary if atomic is False
            - used to aggregate outer indicators
        decision_list: List[Tuple[BaseTradeDecision, pd.Timestamp, pd.Timestamp]] = None,
            The decision list of the inner level: List[Tuple[<decision>, <start_time>, <end_time>]]
            The inner level
        indicator_config : dict, optional
            config of calculating indicators, by default {}
        """
        if atomic is True and trade_info is None:
            raise ValueError("trade_info is necessary in atomic executor")
        elif atomic is False and inner_order_indicators is None:
            raise ValueError("inner_order_indicators is necessary in un-atomic executor")

        # update current position and hold bar count in each bar end
        self.update_current_position(trade_start_time, trade_end_time, trade_exchange)

        if self.is_port_metr_enabled():
            # portfolio_metrics is portfolio related analysis
            self.update_portfolio_metrics(trade_start_time, trade_end_time)
            self.update_hist_positions(trade_start_time)

        # update indicator in each bar end
        self.update_indicator(
            trade_start_time=trade_start_time,
            trade_exchange=trade_exchange,
            atomic=atomic,
            outer_trade_decision=outer_trade_decision,
            trade_info=trade_info,
            inner_order_indicators=inner_order_indicators,
            decision_list=decision_list,

View on GitHub (pinned to 79633dd950)

Solutions

  1. Call D.features(..., disk_cache=0) — bypasses the dataset cache and applies inst_processors on freshly loaded data
  2. Disable dataset caching entirely: qlib.init(dataset_cache=None)
  3. Alternatively drop inst_processors and apply the processing after data retrieval (e.g. in your DataHandlerLP processor chain) so the cache stays valid

Example fix

# before
D.features(insts, fields, start, end, inst_processors=[MyProcessor()])  # dataset cache on -> ValueError

# after
D.features(insts, fields, start, end, disk_cache=0, inst_processors=[MyProcessor()])
Defensive patterns

Strategy: validation

Validate before calling

# decide disk_cache based on processor usage
disk_cache = 0 if inst_processors else 1
D.features(insts, fields, start, end, disk_cache=disk_cache, inst_processors=inst_processors)

Type guard

def needs_disk_cache_bypass(inst_processors) -> bool:
    return bool(inst_processors)

Try / catch

try:
    df = D.features(insts, fields, start, end, inst_processors=procs)
except ValueError as e:
    if "does not support inst_processor" in str(e):
        df = D.features(insts, fields, start, end, disk_cache=0, inst_processors=procs)
    else:
        raise

Prevention

When it happens

Trigger: Calling D.features(instruments, fields, ..., inst_processors=[...]) while a dataset cache is configured and disk_cache is left at its default (1); or DatasetCache.dataset(...) reaching _dataset with truthy inst_processors and disk_cache != 0.

Common situations: Using inst_processors like ProcessInstrument (e.g. CSZScoreNorm-style per-instrument transforms at data-loading time) together with dataset caching enabled; the cache would silently return unprocessed data if this guard were absent — hence the hard error. Related FIXME: resampled cache read back with end_time truncation can also yield incomplete dates.

Related errors


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