Skip to content

Improve backtesting data memory reuse#1108

Open
mpelteshki wants to merge 3 commits into
devfrom
perf/backtesting-data-efficiency
Open

Improve backtesting data memory reuse#1108
mpelteshki wants to merge 3 commits into
devfrom
perf/backtesting-data-efficiency

Conversation

@mpelteshki

@mpelteshki mpelteshki commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Reuse the existing DatetimeIndex backing array for Data datetime datalines instead of allocating a duplicate object ndarray during repair.
  • Keep intraday open/close selection fast by comparing against the existing nanosecond cursor/index values.
  • Reuse the shared default USD quote asset when PandasData normalizes data-store keys.

Benchmark

Local harness: 120k 1-minute OHLCV rows, 20k get_last_price() calls, 4k get_bars() calls.

  • Data.repair_times_and_fill() median: 0.378902s before -> 0.006489s after
  • traced peak during repair: 21.26 MB before -> 4.96 MB after
  • retained traced memory after repair: 19.23 MB before -> 3.87 MB after
  • get_last_price() loop: 0.077269s before -> 0.078164s after
  • get_bars() loop: 0.055513s before -> 0.054401s after
  • checksum unchanged: 2,399,990; rows seen unchanged: 80,000

Tests

  • .venv/bin/python -m pytest tests/test_memory_efficiency_entities_backtesting.py tests/test_data_entity.py tests/test_data_iter_count_microsecond_index.py tests/test_pandas_data_find_asset_timestep_match.py
  • .venv/bin/python -m pytest tests/test_get_historical_prices.py tests/test_data_timeshift_none.py tests/test_data_get_bars_day_includes_latest_completed_bar.py tests/test_backtesting_pandas_daily_routing.py
  • .venv/bin/python -m pytest tests/test_data_source.py tests/test_backtesting_broker.py

Summary by CodeRabbit

  • Performance
    • Optimized backtesting data access and reduced redundant datetime/dataframe work to lower memory use on large datasets.
    • Reused a cached default USD forex quote when no quote is specified for asset lookups.
    • Improved Data bar slicing/resampling to avoid retaining large timestamp/index structures when not needed.
  • Bug Fixes
    • Ensured resampled bars omit non-OHLCV columns.
    • Improved broker order reconciliation for duplicate order identifiers.
  • Tests
    • Added regression coverage for memory/index behavior, large repair_times_and_fill() cases, order indexing (including duplicates), and resampling with extra columns.

@mpelteshki mpelteshki requested a review from grzesir as a code owner July 4, 2026 17:04
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be818891-6ceb-449f-bf9a-24c92710b5c2

📥 Commits

Reviewing files that changed from the base of the PR and between ec56ea6 and 2f977d0.

📒 Files selected for processing (2)
  • lumibot/entities/data.py
  • lumibot/strategies/_strategy.py
✅ Files skipped from review due to trivial changes (1)
  • lumibot/strategies/_strategy.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lumibot/entities/data.py

📝 Walkthrough

Walkthrough

This PR updates backtesting data access to reuse cached assets and index structures, refactors Data.get_bars() around row-bound window helpers, indexes orders by identifier during broker sync, and adds tests plus changelog entries.

Changes

Backtesting performance optimizations and order indexing

Layer / File(s) Summary
Cached USD forex singleton
lumibot/data_sources/pandas_data.py, tests/test_pandas_data_find_asset_timestep_match.py
_set_pandas_data_keys() reuses _USD_FOREX when no quote is present, and the test checks the singleton identity.
Lazy iter_index and bounded iter_index_dict
lumibot/entities/data.py, tests/test_memory_efficiency_entities_backtesting.py
iter_index becomes a property, iter_index_dict is conditionally built, to_datalines() reuses df.index.array, get_last_price() uses cached cursor/index values, and tests cover repair-time memory behavior and datetime array reuse.
Bar-request row-bound slicing helpers
lumibot/entities/data.py, tests/test_data_entity.py
New helpers compute bar windows from row bounds, get_bars() uses them across native fast paths and strict timestep handling, and the resampling test confirms non-OHLCV columns are dropped.
Order identifier indexing for broker sync
lumibot/strategies/strategy_executor.py, tests/test_strategy_executor_order_index.py
sync_broker uses _index_orders_by_identifier for identifier lookups and duplicate handling, with tests for unique and duplicate identifiers.
Changelog
CHANGELOG.md
Adds 4.5.66 unreleased notes for the performance changes and added regression coverage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Data
  participant DataFrameIndex

  Caller->>Data: repair_times_and_fill()
  Data->>Data: check dataset size vs _ITER_INDEX_DICT_MAX_ROWS
  alt small dataset
    Data->>DataFrameIndex: build iter_index_dict
  else large dataset
    Data->>Data: set iter_index_dict = {}
  end
  Caller->>Data: access iter_index
  Data->>DataFrameIndex: reconstruct pd.Series from df.index
  Data-->>Caller: iter_index view
  Caller->>Data: to_datalines()
  Data->>DataFrameIndex: reuse df.index.array
  Data-->>Caller: datetime dataline
Loading
sequenceDiagram
  participant sync_broker
  participant _index_orders_by_identifier
  participant BrokerOrders
  participant LumibotOrders

  sync_broker->>_index_orders_by_identifier: build identifier map from LumibotOrders
  _index_orders_by_identifier-->>sync_broker: dict of identifier -> Order or list[Order]
  loop for each broker order
    sync_broker->>sync_broker: lookup identifier in map
    alt list of orders
      sync_broker->>sync_broker: reconcile duplicates
    else single order
      sync_broker->>sync_broker: reconcile single match
    end
  end
Loading

Possibly related PRs

  • Lumiwealth/lumibot#1092: Both PRs modify Data.get_bars() resampling and intraday timestep handling, including strict request behavior and partial-bucket processing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the PR’s main focus on reducing backtesting data memory usage and reuse.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/backtesting-data-efficiency

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.6)
lumibot/entities/data.py

************* Module pylintrc
pylintrc:1:0: F0011: error while parsing the configuration: File contains no section headers.
file: 'pylintrc', line: 1
'known-third-party=lumibot' (config-parse-error)
[
{
"type": "convention",
"module": "lumibot.entities.data",
"obj": "",
"line": 50,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/entities/data.py",
"symbol": "line-too-long",
"message": "Line too long (103/100)",
"message-id": "C0301"
},
{
"type": "convention",
"module": "lumibot.entities.data",
"obj": "",
"line": 191,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/entities/data.py",
"symbol": "line-too-long",
"message": "Line too long (110/100)",
"message-id": "C0301"
},
{
"type": "convention",
"module": "lumibot.entities.data",
"obj"

... [truncated 70083 characters] ...

"module": "lumibot.entities.data",
"obj": "Data.get_bars",
"line": 1442,
"column": 12,
"endLine": 1442,
"endColumn": 41,
"path": "lumibot/entities/data.py",
"symbol": "attribute-defined-outside-init",
"message": "Attribute '_get_bars_slice_cache_df' defined outside init",
"message-id": "W0201"
},
{
"type": "warning",
"module": "lumibot.entities.data",
"obj": "Data.get_bars",
"line": 1530,
"column": 12,
"endLine": 1530,
"endColumn": 41,
"path": "lumibot/entities/data.py",
"symbol": "attribute-defined-outside-init",
"message": "Attribute '_get_bars_slice_cache_df' defined outside init",
"message-id": "W0201"
}
]

lumibot/strategies/_strategy.py

************* Module pylintrc
pylintrc:1:0: F0011: error while parsing the configuration: File contains no section headers.
file: 'pylintrc', line: 1
'known-third-party=lumibot' (config-parse-error)
[
{
"type": "convention",
"module": "lumibot.strategies._strategy",
"obj": "",
"line": 211,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/strategies/_strategy.py",
"symbol": "line-too-long",
"message": "Line too long (107/100)",
"message-id": "C0301"
},
{
"type": "convention",
"module": "lumibot.strategies._strategy",
"obj": "",
"line": 213,
"column": 0,
"endLine": null,
"endColumn": null,
"path": "lumibot/strategies/_strategy.py",
"symbol": "line-too-long",
"message": "Line too long (109/100)",
"message-id": "C0301"
},
{
"type": "convention",
"module": "lumibot.

... [truncated 187522 characters] ...

ng",
"module": "lumibot.strategies._strategy",
"obj": "_Strategy.load_variables_from_db",
"line": 4206,
"column": 16,
"endLine": 4206,
"endColumn": 30,
"path": "lumibot/strategies/_strategy.py",
"symbol": "attribute-defined-outside-init",
"message": "Attribute 'db_engine' defined outside init",
"message-id": "W0201"
},
{
"type": "refactor",
"module": "lumibot.strategies._strategy",
"obj": "_Strategy",
"line": 139,
"column": 0,
"endLine": 139,
"endColumn": 15,
"path": "lumibot/strategies/_strategy.py",
"symbol": "too-many-public-methods",
"message": "Too many public methods (21/20)",
"message-id": "R0904"
}
]


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lumibot/entities/data.py (1)

1367-1384: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Native fast paths should keep the same data-availability guard

The native_multi_minute and native_1 branches still call _get_bars_frame_window() directly, so they bypass the strict_end_check/stale-bar handling and "outside available data" warnings that the resample path gets from _validate_bars_request(). If this is intentional, it needs its own guard; otherwise call _validate_bars_request() before taking the fast path. [end]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lumibot/entities/data.py` around lines 1367 - 1384, The native fast path in
the data retrieval logic is bypassing the same availability checks used by the
resample path. Update the `native_multi_minute`/`native_1` handling in `Data` so
it runs `_validate_bars_request()` before calling `_get_bars_frame_window()`, or
add an equivalent guard there if the fast path must remain separate. Make sure
the existing `strict_end_check`, stale-bar behavior, and “outside available
data” warning semantics stay consistent across these branches.
🧹 Nitpick comments (1)
lumibot/entities/data.py (1)

1384-1443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated NaN/column-presence post-processing block across both native fast paths.

The ~40-line block handling _bars_has_volume/_bars_has_dividend/_bars_has_stock_splits caching, NaN fill, and dropna is now duplicated verbatim between the "native_multi_minute" and "native_1" fast paths. Since this refactor already touched both blocks to swap in _get_bars_frame_window(), extracting this shared post-processing into a small helper (e.g. _finalize_bars_slice(df)) would reduce future drift risk between the two paths.

Also applies to: 1488-1531

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lumibot/entities/data.py` around lines 1384 - 1443, The NaN/column-presence
post-processing logic is duplicated in both native fast paths, which risks drift
between the "native_multi_minute" and "native_1" branches. Extract the shared
block that uses _bars_has_volume, _bars_has_dividend, _bars_has_stock_splits,
_bars_required_cols, and the NaN-fill/dropna logic into a small helper such as
_finalize_bars_slice(df), then call that helper from both code paths. Keep the
cache key behavior and returned DataFrame semantics unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lumibot/entities/data.py`:
- Around line 1300-1324: The `_get_bars_frame_window` helper currently depends
on pandas’ private `_slice()` method, which can break on future pandas changes.
Update this logic to use the public indexing path in `_get_bars_frame_window` on
the cached source frame returned by `_get_bars_source_frame`, using a
`.iloc[start_row:end_row]` fallback for the window extraction. Keep the existing
`start_row`, `end_row`, and `normalized_timeshift` behavior intact so
`get_bars()` continues to return the same results.

---

Outside diff comments:
In `@lumibot/entities/data.py`:
- Around line 1367-1384: The native fast path in the data retrieval logic is
bypassing the same availability checks used by the resample path. Update the
`native_multi_minute`/`native_1` handling in `Data` so it runs
`_validate_bars_request()` before calling `_get_bars_frame_window()`, or add an
equivalent guard there if the fast path must remain separate. Make sure the
existing `strict_end_check`, stale-bar behavior, and “outside available data”
warning semantics stay consistent across these branches.

---

Nitpick comments:
In `@lumibot/entities/data.py`:
- Around line 1384-1443: The NaN/column-presence post-processing logic is
duplicated in both native fast paths, which risks drift between the
"native_multi_minute" and "native_1" branches. Extract the shared block that
uses _bars_has_volume, _bars_has_dividend, _bars_has_stock_splits,
_bars_required_cols, and the NaN-fill/dropna logic into a small helper such as
_finalize_bars_slice(df), then call that helper from both code paths. Keep the
cache key behavior and returned DataFrame semantics unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4d43f3f-0cb8-45e0-a84b-96a2288ef570

📥 Commits

Reviewing files that changed from the base of the PR and between 51e88d6 and ec56ea6.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • lumibot/data_sources/pandas_data.py
  • lumibot/entities/data.py
  • lumibot/strategies/strategy_executor.py
  • tests/test_data_entity.py
  • tests/test_memory_efficiency_entities_backtesting.py
  • tests/test_pandas_data_find_asset_timestep_match.py
  • tests/test_strategy_executor_order_index.py

Comment thread lumibot/entities/data.py
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.

1 participant