Skip to content

BUG: Size shared memory by materialized array, not lazy Index.nbytes - #1385

Merged
kernc merged 1 commit into
kernc:masterfrom
nyxst4ck:fix-shm-rangeindex-nbytes
Aug 5, 2026
Merged

BUG: Size shared memory by materialized array, not lazy Index.nbytes#1385
kernc merged 1 commit into
kernc:masterfrom
nyxst4ck:fix-shm-rangeindex-nbytes

Conversation

@nyxst4ck

@nyxst4ck nyxst4ck commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #1237.

The bug

Backtest.optimize(method='grid') fails with TypeError: buffer is too small for requested array whenever the data has a RangeIndex, while bt.run() on the same data works fine.

Root cause in SharedMemoryManager.arr2shm (backtesting/_util.py): the shared-memory block is sized with vals.nbytes, but RangeIndex is lazy — its .nbytes reports the size of the underlying range object itself (a constant 132 bytes), not the int64 array it materializes into. The very next line builds np.ndarray(vals.shape, dtype=vals.dtype.base, buffer=shm.buf), which needs 8 × len(index) bytes. Measured: pd.RangeIndex(44640).nbytes == 132 vs 357 120 bytes required — so any range-indexed frame longer than 16 rows overflows.

Contrary to the issue title, size is not the real variable — index type is. The "under 10 000 rows works" observations in the thread were almost certainly datetime-indexed runs, and range-indexed data is an explicitly documented input (Backtest.__init__: "a monotonic range index (i.e. a sequence of periods)").

The fix

Size the block from the same shape/dtype.base pair the buffer view is built with: vals.size * vals.dtype.base.itemsize. For every eager index in play (Index[int64], DatetimeIndex, tz-aware DatetimeIndex, float Series) this equals .nbytes, so only the broken RangeIndex path changes behavior.

Validation

  • New regression test test_optimize_range_index (placed next to the closely-related test_optimize_datetime_index_with_timezone): red on master with the exact TypeError from the issue, green with the fix.
  • Correctness beyond not-crashing: with a real multiprocessing.Pool (not the Windows thread fallback), a 4-point grid on range-indexed GOOG returns the same best equity (81 812.37) as four independently computed serial bt.run() calls.
  • python -m backtesting.test: 81 tests, OK (1 skipped).
  • flake8: clean. mypy: error list byte-identical to master (all pre-existing, none introduced).

RangeIndex is lazy: its .nbytes reports the size of the range object
itself (a constant 132 bytes), not the int64 array it materializes
into. SharedMemoryManager.arr2shm sized the block with vals.nbytes and
then built an ndarray of vals.shape/vals.dtype.base over the buffer, so
Backtest.optimize() on range-indexed data raised 'TypeError: buffer is
too small for requested array' while bt.run() on the same data worked.

Size the block from the same shape/dtype pair the buffer view is built
with. For eager indexes (Index[int64], DatetimeIndex, tz-aware) the
expression equals .nbytes, so only the broken RangeIndex path changes.

Fixes kernc#1237
@kernc

kernc commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Excellent work! Many thanks!

@kernc
kernc merged commit ca2e261 into kernc:master Aug 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bt.optimize() results in "buffer is too small for requested array"

2 participants