{"record":{"id":"84fe3be91abce96c","repo":"microsoft/qlib","slug":"execution-volume-is-invalid-exec-vol-position","errorCode":null,"errorMessage":"Execution volume is invalid: {exec_vol} (position = {self.position})","messagePattern":"Execution volume is invalid: (.+?) \\(position = (.+?)\\)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"qlib/rl/order_execution/simulator_simple.py","lineNumber":169,"sourceCode":"        ----------\n        amount\n            The amount you wish to deal. The simulator doesn't guarantee all the amount to be successfully dealt.\n        \"\"\"\n\n        assert not self.done()\n\n        self.market_price = self.market_vol = None  # avoid misuse\n        exec_vol = self._split_exec_vol(amount)\n        assert self.market_price is not None\n        assert self.market_vol is not None\n\n        ticks_position = self.position - np.cumsum(exec_vol)\n\n        self.position -= exec_vol.sum()\n        if abs(self.position) < 1e-6:\n            self.position = 0.0\n        if self.position < -EPS or (exec_vol < -EPS).any():\n            raise ValueError(f\"Execution volume is invalid: {exec_vol} (position = {self.position})\")\n\n        # Get time index available for this step\n        time_index = self._get_ticks_slice(self.cur_time, self._next_time())\n\n        self.history_exec = self._dataframe_append(\n            self.history_exec,\n            SAOEMetrics(\n                # It should have the same keys with SAOEMetrics,\n                # but the values do not necessarily have the annotated type.\n                # Some values could be vectorized (e.g., exec_vol).\n                stock_id=self.order.stock_id,\n                datetime=time_index,\n                direction=self.order.direction,\n                market_volume=self.market_vol,\n                market_price=self.market_price,\n                amount=exec_vol,\n                inner_amount=exec_vol,\n                deal_amount=exec_vol,","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/microsoft/qlib/blob/79633dd9506ea689e5400dea0197717b5b3d74b7/qlib/rl/order_execution/simulator_simple.py#L151-L187","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["In the interpreter's `action_to_simulator_action`, clip volumes to `[0, remaining_position]` (and `>= 0`) before returning.","Compute volumes as differences of cumulative ratios (`cumsum[i] - cumsum[i-1]`) rather than independent ratios so they sum exactly to the order amount.","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."],"exampleFix":"// before\nexec_vol = amount  # amount may exceed remaining position or be negative\n// after\nexec_vol = np.clip(amount, 0.0, self.position)  # never over-sell, never negative","handlingStrategy":"validation","validationCode":"import numpy as np\nEPS = 1e-6\n\ndef sanitize_exec_vol(exec_vol: np.ndarray, remaining: float) -> np.ndarray:\n    exec_vol = np.clip(exec_vol, 0.0, None)          # no negative volume\n    exec_vol = np.minimum(exec_vol, remaining)        # never exceed remaining position\n    return exec_vol","typeGuard":"def exec_vol_valid(exec_vol, remaining: float, eps: float = 1e-6) -> bool:\n    import numpy as np\n    exec_vol = np.asarray(exec_vol)\n    return bool((exec_vol >= -eps).all() and exec_vol.sum() <= remaining + eps)","tryCatchPattern":"try:\n    sim.step(action)\nexcept ValueError as e:\n    if \"Execution volume is invalid\" in str(e):\n        log.error(\"over-sell at step %d: position=%s\", sim.cur_step, sim.position)\n        raise\n    raise","preventionTips":["Always clip interpreter output volumes to [0, remaining_order_amount].","Derive per-tick volumes from cumulative-ratio differences, not independent ratios.","Log position and exec_vol every step during strategy debugging."],"tags":["rl","order-execution","simulator","action-clipping","invariant"],"backgroundTag":null,"analyzedSha":"79633dd9506ea689e5400dea0197717b5b3d74b7","analyzedAt":"2026-08-15T07:01:27.511Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}