microsoft/qlib · error · NotImplementedError

direction not supported, `Order.SELL` for sell, `Order.BUY`

Error message

direction not supported, `Order.SELL` for sell, `Order.BUY` for buy

What it means

DatasetURICache/IndexManager.sync_to_disk (qlib/data/cache.py:815) persists the in-memory cache index to an HDF5 .index file; it refuses when self._data is None, i.e. no index was ever loaded (sync_from_disk was not called / no data was set). This prevents writing an empty/garbage index file that would masquerade as a valid cache index.

Source

Thrown at qlib/backtest/decision.py:85

    # What the value should be about in all kinds of cases
    # - not tradable: the deal_amount == 0 , factor is None
    #    - the stock is suspended and the entire order fails. No cost for this order
    # - dealt or partially dealt: deal_amount >= 0 and factor is not None
    deal_amount: float = 0.0  # `deal_amount` is a non-negative value
    factor: Optional[float] = None

    # TODO:
    # a status field to indicate the dealing result of the order

    # FIXME:
    # for compatible now.
    # Please remove them in the future
    SELL: ClassVar[OrderDir] = OrderDir.SELL
    BUY: ClassVar[OrderDir] = OrderDir.BUY

    def __post_init__(self) -> None:
        if self.direction not in {Order.SELL, Order.BUY}:
            raise NotImplementedError("direction not supported, `Order.SELL` for sell, `Order.BUY` for buy")
        self.deal_amount = 0.0
        self.factor = None

    @property
    def amount_delta(self) -> float:
        """
        return the delta of amount.
        - Positive value indicates buying `amount` of share
        - Negative value indicates selling `amount` of share
        """
        return self.amount * self.sign

    @property
    def deal_amount_delta(self) -> float:
        """
        return the delta of deal_amount.
        - Positive value indicates buying `deal_amount` of share
        - Negative value indicates selling `deal_amount` of share

View on GitHub (pinned to 79633dd950)

Solutions

  1. Call index_manager.get_index(...) or sync_from_disk() first so _data is populated, then sync_to_disk()
  2. Or use IndexManager.update(data, sync=True) which sets _data then syncs in one step
  3. If the .index file does not exist yet, generate the cache (DatasetCache.update / gen_dataset_cache) through public APIs instead of hand-driving IndexManager

Example fix

# before
im = IndexManager(cache_path)
im.sync_to_disk()  # ValueError: No data to sync to disk.

# after
im = IndexManager(cache_path)
idx = im.get_index()      # triggers sync_from_disk, populates _data
im.sync_to_disk()
Defensive patterns

Strategy: validation

Validate before calling

im = IndexManager(cache_path)
if im._data is None:
    im.sync_from_disk()  # or im.get_index() — populates _data first
im.sync_to_disk()

Type guard

def index_ready(im) -> bool:
    return im._data is not None

Try / catch

try:
    im.sync_to_disk()
except ValueError as e:
    if "No data to sync" in str(e):
        im.sync_from_disk()
        im.sync_to_disk()
    else:
        raise

Prevention

When it happens

Trigger: Constructing IndexManager(cache_path) and calling sync_to_disk() before get_index()/sync_from_disk()/update() has populated _data; typically in custom code driving the dataset-URI index, or a code path where sync_from_disk found no '/df' key (empty DataFrame case still sets _data, so the None case means load was skipped entirely).

Common situations: Custom maintenance scripts touching qlib's cache index; edge flows where a fresh cache directory has no .index file yet and the caller tries to write before any read/update.

Related errors


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