microsoft/qlib · error · ValueError

Execution volume is invalid: {exec_vol} (position = {self.po

Error message

Execution volume is invalid: {exec_vol} (position = {self.position})

What it means

Runtime sanity check in `SimpleOrderExecutionSimulator.step` (qlib/rl/order_execution/simulator_simple.py:169). After applying the execution volumes for this tick, either the resulting position went below `-EPS` (over-sold beyond holdings/order size) or some individual execution volume was negative. It means the action interpreter produced volumes inconsistent with the order and simulator state.

Source

Thrown at qlib/rl/order_execution/simulator_simple.py:169

        ----------
        amount
            The amount you wish to deal. The simulator doesn't guarantee all the amount to be successfully dealt.
        """

        assert not self.done()

        self.market_price = self.market_vol = None  # avoid misuse
        exec_vol = self._split_exec_vol(amount)
        assert self.market_price is not None
        assert self.market_vol is not None

        ticks_position = self.position - np.cumsum(exec_vol)

        self.position -= exec_vol.sum()
        if abs(self.position) < 1e-6:
            self.position = 0.0
        if self.position < -EPS or (exec_vol < -EPS).any():
            raise ValueError(f"Execution volume is invalid: {exec_vol} (position = {self.position})")

        # Get time index available for this step
        time_index = self._get_ticks_slice(self.cur_time, self._next_time())

        self.history_exec = self._dataframe_append(
            self.history_exec,
            SAOEMetrics(
                # It should have the same keys with SAOEMetrics,
                # but the values do not necessarily have the annotated type.
                # Some values could be vectorized (e.g., exec_vol).
                stock_id=self.order.stock_id,
                datetime=time_index,
                direction=self.order.direction,
                market_volume=self.market_vol,
                market_price=self.market_price,
                amount=exec_vol,
                inner_amount=exec_vol,
                deal_amount=exec_vol,

View on GitHub (pinned to 79633dd950)

Solutions

  1. In the interpreter's `action_to_simulator_action`, clip volumes to `[0, remaining_position]` (and `>= 0`) before returning.
  2. Compute volumes as differences of cumulative ratios (`cumsum[i] - cumsum[i-1]`) rather than independent ratios so they sum exactly to the order amount.
  3. Print `self.position` and `exec_vol` in a debug run to find the first step where the invariant breaks and inspect the policy's raw action for that step.

Example fix

// before
exec_vol = amount  # amount may exceed remaining position or be negative
// after
exec_vol = np.clip(amount, 0.0, self.position)  # never over-sell, never negative
Defensive patterns

Strategy: validation

Validate before calling

import numpy as np
EPS = 1e-6

def sanitize_exec_vol(exec_vol: np.ndarray, remaining: float) -> np.ndarray:
    exec_vol = np.clip(exec_vol, 0.0, None)          # no negative volume
    exec_vol = np.minimum(exec_vol, remaining)        # never exceed remaining position
    return exec_vol

Type guard

def exec_vol_valid(exec_vol, remaining: float, eps: float = 1e-6) -> bool:
    import numpy as np
    exec_vol = np.asarray(exec_vol)
    return bool((exec_vol >= -eps).all() and exec_vol.sum() <= remaining + eps)

Try / catch

try:
    sim.step(action)
except ValueError as e:
    if "Execution volume is invalid" in str(e):
        log.error("over-sell at step %d: position=%s", sim.cur_step, sim.position)
        raise
    raise

Prevention

When it happens

Trigger: An ActionInterpreter whose converted action sums to more than the remaining order amount (position goes negative), or a twins/split scheme yielding a negative volume component. Also triggered by a policy outputting extreme actions that survive the interpreter without clipping.

Common situations: Custom order-execution interpreters that forget to clamp per-tick volumes; orders smaller than the minimum execution unit; floating-point drift accumulating over many steps when volumes are computed by ratios instead of differences.

Related errors


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