Skip to content

Add Alpaca live broker strategy CI - #1104

Closed
mpelteshki wants to merge 9 commits into
devfrom
ci/alpaca-live-broker-tests
Closed

Add Alpaca live broker strategy CI#1104
mpelteshki wants to merge 9 commits into
devfrom
ci/alpaca-live-broker-tests

Conversation

@mpelteshki

@mpelteshki mpelteshki commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary:

  • Add a Blacksmith-backed Alpaca Live Broker workflow for internal pushes/PRs and manual runs.
  • Add real Alpaca paper strategy apitests that call run_live(run_once=True), read broker data, fetch SPY option chains, submit a non-marketable AAPL limit order, and cancel it.
  • Run MCP action/transport coverage in the same workflow.

Tests:

  • .venv/bin/ruff check tests/test_alpaca_live_broker_apitest.py
  • .venv/bin/python -m pytest -q tests/test_agent_runtime_remote_mcp.py tests/test_agent_runtime_mcp_transports.py
  • GitHub Actions: Alpaca Live Broker push run 28653427520 passed

@mpelteshki
mpelteshki requested a review from grzesir as a code owner July 3, 2026 10:06
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an Alpaca live-broker workflow and test gating, plus data/backtesting refactors for bar retrieval, duplicate handling, and order reconciliation with matching regression coverage and docs.

Changes

Alpaca live broker CI and tests

Layer / File(s) Summary
Workflow and docs
.github/workflows/alpaca-live-broker.yml, docs/ALPACA_LIVE_BROKER_CI.md, docsrc/environment_variables.rst
Adds the Alpaca live-broker workflow, credential checks, pytest steps, and matching CI/environment documentation.
Alpaca marker gating
tests/conftest.py, tests/test_agent_alpaca_news_live_apitest.py, tests/test_ibkr_futures_daily_series.py
Extends pytest gating for the alpaca marker and updates the Alpaca news live test module’s credential handling and selection.
Live order strategy and test
tests/test_alpaca_live_broker_apitest.py
Adds Alpaca authentication helpers, order status polling, a live order/cancel strategy, and a cancellation-focused live test.
Options chain strategy and test
tests/test_alpaca_live_broker_apitest.py
Adds a live SPY options-chain strategy and a test that verifies chain data, selected option symbol, and stream cleanup.

Data and backtesting performance

Layer / File(s) Summary
Data hot paths
lumibot/entities/data.py, tests/test_data_entity.py, tests/test_memory_efficiency_entities_backtesting.py
Changes Data caching and bar-window retrieval internals, with tests for resampling behavior and retained lookup memory.
Backtesting and order sync
lumibot/backtesting/alpaca_backtesting.py, lumibot/strategies/strategy_executor.py, lumibot/tools/databento_helper.py, lumibot/tools/databento_helper_polars.py, tests/test_databento_helper.py, tests/test_memory_efficiency_entities_backtesting.py, tests/test_strategy_executor_order_index.py, CHANGELOG.md
Refactors Alpaca gap filling, order reconciliation by identifier, Databento duplicate handling, and adds regression tests plus changelog notes.

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

Possibly related PRs

  • Lumiwealth/lumibot#1092: Shares the lumibot/entities/data.py get_bars() resampling and strict timestep path, including related bar-window and resampling changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding CI for Alpaca live broker strategy tests.
✨ 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 ci/alpaca-live-broker-tests

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

🧹 Nitpick comments (2)
.github/workflows/alpaca-live-broker.yml (2)

48-48: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false on checkout.

Static analysis (zizmor) flags credential persistence via actions/checkout (artipacked). Since no later step needs to push using the git token, disable persistence.

🔒 Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 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 @.github/workflows/alpaca-live-broker.yml at line 48, The checkout step in
the Alpaca live broker workflow is persisting git credentials unnecessarily.
Update the existing actions/checkout usage to disable credential persistence by
setting persist-credentials to false, since no later step needs to push with the
token.

Source: Linters/SAST tools


41-46: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a concurrency group to prevent overlapping live-broker runs.

This job submits and cancels real orders against a shared Alpaca paper account. Without a concurrency block, simultaneous pushes/PRs (or a push racing a manual dispatch) could run in parallel and interfere with each other's orders (e.g., one run's cancel_open_orders() cancelling another run's in-flight order), causing flaky failures.

♻️ Proposed concurrency group
 jobs:
   alpaca-live-broker:
     name: Alpaca live broker strategy
     runs-on: blacksmith-2vcpu-ubuntu-2404
     timeout-minutes: 30
     if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
+    concurrency:
+      group: alpaca-live-broker
+      cancel-in-progress: false
🤖 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 @.github/workflows/alpaca-live-broker.yml around lines 41 - 46, The
alpaca-live-broker job is missing a concurrency safeguard, so overlapping runs
can interfere with shared Alpaca paper-account orders. Add a concurrency block
to the alpaca-live-broker workflow job and use a stable group key tied to the
workflow/job and branch or ref so only one live-broker execution runs at a time.
Make sure the new setting is placed alongside the existing alpaca-live-broker
job definition and preserves the current job gating in github.event_name and
github.event.pull_request checks.
🤖 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 `@tests/test_alpaca_live_broker_apitest.py`:
- Around line 1-184: The new live-broker CI test coverage in the Alpaca apitest
module needs matching documentation updates. Add an engineering note in docs/
describing the new live-broker CI workflow, what _require_alpaca,
_LiveOrderDataStrategy, and _LiveOptionsChainStrategy validate, and any CI/env
requirements. Also update docsrc/ with any user-facing testing guidance changes
so contributors know how to run or interpret the live-broker tests.

---

Nitpick comments:
In @.github/workflows/alpaca-live-broker.yml:
- Line 48: The checkout step in the Alpaca live broker workflow is persisting
git credentials unnecessarily. Update the existing actions/checkout usage to
disable credential persistence by setting persist-credentials to false, since no
later step needs to push with the token.
- Around line 41-46: The alpaca-live-broker job is missing a concurrency
safeguard, so overlapping runs can interfere with shared Alpaca paper-account
orders. Add a concurrency block to the alpaca-live-broker workflow job and use a
stable group key tied to the workflow/job and branch or ref so only one
live-broker execution runs at a time. Make sure the new setting is placed
alongside the existing alpaca-live-broker job definition and preserves the
current job gating in github.event_name and github.event.pull_request checks.
🪄 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: 8ff4f70b-8a96-4c9f-bf4e-83bdd84804b3

📥 Commits

Reviewing files that changed from the base of the PR and between b6367da and cf18606.

📒 Files selected for processing (2)
  • .github/workflows/alpaca-live-broker.yml
  • tests/test_alpaca_live_broker_apitest.py

Comment on lines +1 to +184
import time
from datetime import datetime, timedelta

import pytest

from lumibot.brokers.alpaca import Alpaca
from lumibot.components.options_helper import OptionsHelper
from lumibot.credentials import ALPACA_TEST_CONFIG
from lumibot.entities import Asset, Order
from lumibot.strategies.strategy import Strategy

pytestmark = pytest.mark.apitest


def _require_alpaca() -> Alpaca:
api_key = ALPACA_TEST_CONFIG.get("API_KEY")
api_secret = ALPACA_TEST_CONFIG.get("API_SECRET")
if not api_key or not api_secret or api_key == "<your key here>" or api_secret == "<your key here>":
pytest.skip("Missing ALPACA_TEST_API_KEY / ALPACA_TEST_API_SECRET")

broker = Alpaca(ALPACA_TEST_CONFIG, max_workers=1, connect_stream=False)
try:
account = broker.api.get_account()
except Exception as exc:
broker.cleanup_streams()
raise RuntimeError(f"Alpaca paper account authentication failed: {exc}") from exc

if getattr(account, "trading_blocked", False):
broker.cleanup_streams()
pytest.skip("Alpaca paper account is trading-blocked")

return broker


def _status_text(raw_order) -> str:
status = getattr(raw_order, "status", "")
if hasattr(status, "value"):
status = status.value
return str(status).lower()


def _wait_for_terminal_cancel(broker: Alpaca, order_id: str, *, timeout: float = 30.0) -> str:
deadline = time.time() + timeout
last_status = ""
while time.time() < deadline:
raw = broker.api.get_order_by_id(order_id)
last_status = _status_text(raw)
if last_status in {"canceled", "cancelled"} or last_status.endswith(".canceled"):
return last_status
if last_status in {"filled", "rejected", "expired"}:
return last_status
time.sleep(0.25)
return last_status


class _LiveOrderDataStrategy(Strategy):
def initialize(self):
self.sleeptime = "1S"
self.vars.iterations = 0
self.vars.submitted_order_id = None
self.vars.stock_price = None
self.vars.bar_count = 0

def on_trading_iteration(self):
self.vars.iterations += 1

asset = Asset("AAPL", asset_type=Asset.AssetType.STOCK)
price = self.get_last_price(asset)
assert price is not None and float(price) > 0
self.vars.stock_price = float(price)

bars = self.get_historical_prices(asset, 3, "day")
assert bars is not None and bars.df is not None and not bars.df.empty
self.vars.bar_count = len(bars.df)

# Non-marketable by design: CI must prove real submit/cancel wiring without taking a fill.
order = self.create_order(
asset,
1,
Order.OrderSide.BUY,
order_type=Order.OrderType.LIMIT,
limit_price=0.01,
time_in_force="day",
)
submitted = self.submit_order(order)
assert submitted is not None and submitted.identifier
self.vars.submitted_order_id = submitted.identifier
self.broker.cancel_order(submitted)


class _LiveOptionsChainStrategy(Strategy):
def initialize(self):
self.sleeptime = "1S"
self.options_helper = OptionsHelper(self)
self.vars.iterations = 0
self.vars.call_expirations = 0
self.vars.put_expirations = 0
self.vars.option_symbol = None

def on_trading_iteration(self):
self.vars.iterations += 1

underlying = Asset("SPY", asset_type=Asset.AssetType.STOCK)
underlying_price = self.get_last_price(underlying)
assert underlying_price is not None and float(underlying_price) > 0

chains = self.get_chains(underlying)
chain_root = chains.get("Chains", {}) if isinstance(chains, dict) else {}
call_chains = chain_root.get("CALL", {})
put_chains = chain_root.get("PUT", {})
assert call_chains, "Alpaca returned no SPY call chains"
assert put_chains, "Alpaca returned no SPY put chains"
self.vars.call_expirations = len(call_chains)
self.vars.put_expirations = len(put_chains)

target_date = datetime.now().astimezone().date() + timedelta(days=7)
expiry = self.options_helper.get_expiration_on_or_after_date(
target_date,
chains,
"call",
underlying_asset=underlying,
)
assert expiry is not None

expiry_key = expiry.strftime("%Y-%m-%d") if hasattr(expiry, "strftime") else str(expiry)
strikes = call_chains.get(expiry_key)
assert strikes, f"Alpaca returned no SPY call strikes for {expiry_key}"
strike = min(strikes, key=lambda value: abs(float(value) - float(underlying_price)))

option = self.options_helper.find_next_valid_option(underlying, strike, expiry, put_or_call="call")
assert option is not None
self.vars.option_symbol = str(option)


def test_alpaca_run_once_strategy_reads_data_submits_and_cancels_real_order():
broker = _require_alpaca()
strategy = _LiveOrderDataStrategy(
broker=broker,
name="alpaca-live-broker-order-ci",
benchmark_asset=None,
should_backup_variables_to_database=False,
should_send_summary_to_discord=False,
save_logfile=False,
)

try:
strategy.run_live(run_once=True)
assert strategy.vars.iterations == 1
assert strategy.vars.stock_price > 0
assert strategy.vars.bar_count >= 1
assert strategy.vars.submitted_order_id

status = _wait_for_terminal_cancel(broker, strategy.vars.submitted_order_id)
assert status in {"canceled", "cancelled"} or status.endswith(".canceled")

all_orders = broker._pull_broker_all_orders()
assert all_orders is not None
finally:
try:
strategy.cancel_open_orders()
except Exception:
pass
broker.cleanup_streams()


def test_alpaca_run_once_strategy_reads_options_chain_through_broker_data_source():
broker = _require_alpaca()
strategy = _LiveOptionsChainStrategy(
broker=broker,
name="alpaca-live-broker-options-ci",
benchmark_asset=None,
should_backup_variables_to_database=False,
should_send_summary_to_discord=False,
save_logfile=False,
)

try:
strategy.run_live(run_once=True)
assert strategy.vars.iterations == 1
assert strategy.vars.call_expirations > 0
assert strategy.vars.put_expirations > 0
assert strategy.vars.option_symbol
finally:
broker.cleanup_streams()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update docs/ and docsrc/ for the new live-broker CI workflow.

This PR adds new live-broker CI test coverage (a new module + workflow) but no accompanying documentation changes are included here. As per coding guidelines, "Every time you work on LumiBot code, you MUST check and update BOTH documentation locations: docs/ ... for AI agents and contributors, docsrc/ ... for end users." Please add engineering notes (docs/) describing this CI workflow/test suite, and update docsrc/ if it affects user-facing testing guidance.

🤖 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 `@tests/test_alpaca_live_broker_apitest.py` around lines 1 - 184, The new
live-broker CI test coverage in the Alpaca apitest module needs matching
documentation updates. Add an engineering note in docs/ describing the new
live-broker CI workflow, what _require_alpaca, _LiveOrderDataStrategy, and
_LiveOptionsChainStrategy validate, and any CI/env requirements. Also update
docsrc/ with any user-facing testing guidance changes so contributors know how
to run or interpret the live-broker tests.

Source: Coding guidelines

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf18606e79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

)

try:
strategy.run_live(run_once=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ensure CI can run outside market hours

When this workflow is triggered after hours, on weekends, or on market holidays, StrategyExecutor._run_live_once() returns before calling on_trading_iteration() whenever broker.is_market_open() is false, so this run_live(run_once=True) call leaves strategy.vars.iterations at 0 and the following assertions fail without exercising Alpaca. Because .github/workflows/alpaca-live-broker.yml runs on arbitrary pushes/PRs/manual dispatches, please force a 24/7 test market or otherwise bypass the market-hours gate for these live broker smoke tests.

Useful? React with 👍 / 👎.

@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)
.github/workflows/alpaca-live-broker.yml (1)

85-92: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Credential check omits Alpaca news creds used by the same job.

This step only verifies ALPACA_TEST_API_KEY/ALPACA_TEST_API_SECRET, but the very next step also runs tests/test_agent_alpaca_news_live_apitest.py, which depends on ALPACA_NEWS_API_KEY/ALPACA_NEWS_API_SECRET (set at Lines 53-54). If the news secrets are missing, the failure will surface deep inside pytest instead of the clear early ::error:: message this step is meant to provide.

♻️ Proposed fix
       - name: Verify Alpaca paper credentials are available
         run: |
           set -euo pipefail
           if [ -z "${ALPACA_TEST_API_KEY}" ] || [ -z "${ALPACA_TEST_API_SECRET}" ]; then
             echo "::error::ALPACA_TEST_API_KEY and ALPACA_TEST_API_SECRET repository secrets are required for Alpaca live broker CI."
             exit 1
           fi
+          if [ -z "${ALPACA_NEWS_API_KEY}" ] || [ -z "${ALPACA_NEWS_API_SECRET}" ]; then
+            echo "::error::ALPACA_NEWS_API_KEY and ALPACA_NEWS_API_SECRET repository secrets are required for the Alpaca news live apitest."
+            exit 1
+          fi
🤖 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 @.github/workflows/alpaca-live-broker.yml around lines 85 - 92, The
credential validation step only checks the Alpaca paper secrets, but the same
workflow later runs the Alpaca news live test that also needs the news API
secrets. Update the verification block in the workflow job to validate both
ALPACA_TEST_API_KEY/ALPACA_TEST_API_SECRET and
ALPACA_NEWS_API_KEY/ALPACA_NEWS_API_SECRET, and keep the early ::error:: message
clear by mentioning all required secrets. Use the existing job step and the
downstream test step names to locate the change.
🤖 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 @.github/workflows/alpaca-live-broker.yml:
- Line 60: The job-level timeout on the workflow is shorter than the combined
per-step budgets in the live broker job, so the job can be killed before the
later step timeouts in the run steps ever trigger. Update the job timeout in the
alpaca-live-broker workflow to comfortably exceed the longest possible total
runtime for the steps that use timeout 1500 and timeout 1200, including setup
overhead, so the step-level timeout handling in the live broker
submission/cancellation flow can complete. Refer to the live broker job
definition and its run steps in the workflow to make the adjustment consistently
across the affected section.

---

Outside diff comments:
In @.github/workflows/alpaca-live-broker.yml:
- Around line 85-92: The credential validation step only checks the Alpaca paper
secrets, but the same workflow later runs the Alpaca news live test that also
needs the news API secrets. Update the verification block in the workflow job to
validate both ALPACA_TEST_API_KEY/ALPACA_TEST_API_SECRET and
ALPACA_NEWS_API_KEY/ALPACA_NEWS_API_SECRET, and keep the early ::error:: message
clear by mentioning all required secrets. Use the existing job step and the
downstream test step names to locate the change.
🪄 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: 8fe149d3-5f0e-4e21-b0e2-bd6532b5d68e

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb8791 and 4f02694.

📒 Files selected for processing (1)
  • .github/workflows/alpaca-live-broker.yml

Comment thread .github/workflows/alpaca-live-broker.yml Outdated
@mpelteshki mpelteshki changed the title Add Alpaca live broker strategy CI Improve backtesting data efficiency and Alpaca live CI Jul 4, 2026
@mpelteshki mpelteshki changed the title Improve backtesting data efficiency and Alpaca live CI Add Alpaca live broker strategy CI Jul 4, 2026

@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

🧹 Nitpick comments (3)
lumibot/entities/data.py (2)

1244-1256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Timeshift-to-rows conversion is now triplicated.

The same timedelta→row conversion logic exists in check_data.checker (Lines 876-883), _get_bars_dict (Lines 1156-1162), and the new _normalize_timeshift_to_rows. Consider having the first two call self._normalize_timeshift_to_rows(timeshift) to avoid the three copies drifting apart over time.

🤖 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 1244 - 1256, The timeshift-to-rows
conversion logic is duplicated in `check_data.checker` and `_get_bars_dict`,
alongside the new `_normalize_timeshift_to_rows` helper. Update those existing
callers to use `self._normalize_timeshift_to_rows(timeshift)` instead of
re-implementing the `timedelta` conversion, so the shared logic stays in one
place and remains consistent.

1305-1313: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralized reliance on private pandas API _slice().

df_source._slice(slice(start_row, end_row)) uses DataFrame._slice, an internal/private pandas method (the pandas.core namespace is documented as private and not covered by pandas' API stability guarantees). This helper is now the single choke point used by all three get_bars() fast paths, so any pandas internal change to _slice would break bar retrieval everywhere at once. .iloc[start_row:end_row] is the public equivalent and should have negligible overhead difference.

♻️ Suggested fix
-        df = df_source._slice(slice(start_row, end_row))
+        df = df_source.iloc[start_row:end_row]
🤖 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 1305 - 1313, Replace the private
pandas call in _get_bars_frame_window with the public slicing API, since
df_source._slice(slice(start_row, end_row)) is the centralized fast-path used by
get_bars() and depends on an unstable internal method. Update the slice
operation in _get_bars_frame_window to use the equivalent public indexer on
df_source, keeping the same start_row/end_row behavior and return contract so
the three fast paths continue to work unchanged.
lumibot/tools/databento_helper_polars.py (1)

580-585: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider logging when duplicate timestamps are dropped.

Duplicate ts_event rows are silently collapsed (keeping the last) with no log signal. As per coding guidelines, **/{backtesting,data}/**/*.py: "NEVER fabricate, synthesize, or forward-fill missing market data in backtesting. Return empty data, explicit warnings, or skip strategies." Silently dropping duplicate market-data rows without a warning can hide a real data-provider issue; a logger.warning/debug with the duplicate count would aid diagnosis without changing behavior.

📝 Suggested addition
         if df.index.has_duplicates:
+            dup_count = int(df.index.duplicated().sum())
+            logger.warning("Dropping %d duplicate timestamp(s) from DataBento frame (keeping last).", dup_count)
             df = df[~df.index.duplicated(keep="last")]
🤖 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/tools/databento_helper_polars.py` around lines 580 - 585, The
duplicate-timestamp cleanup in the df.index.has_duplicates branch silently drops
rows, so add a logger.warning or logger.debug before deduping to report how many
duplicate ts_event rows were found and that keep="last" is being applied. Use
the existing helper around the index-normalization logic in
databento_helper_polars.py so the behavior stays the same while making the data
issue visible.

Source: Coding guidelines

🤖 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 1239-1313: The native slice fast paths in Data.get_bars() are
bypassing the strict availability checks, so strict_end_check,
_strict_intraday_stale_bar_error, and the usual out-of-range ValueError handling
never run. Update the native branches to invoke the same strict validation used
by the resample path before calling _slice(), likely by reusing
_validate_bars_request() and the row-bound helpers such as
_get_bars_row_bounds()/_get_bars_frame_window(). Keep the fast path, but ensure
strict callers receive the same stale-bar and bounds behavior as non-native
requests.

---

Nitpick comments:
In `@lumibot/entities/data.py`:
- Around line 1244-1256: The timeshift-to-rows conversion logic is duplicated in
`check_data.checker` and `_get_bars_dict`, alongside the new
`_normalize_timeshift_to_rows` helper. Update those existing callers to use
`self._normalize_timeshift_to_rows(timeshift)` instead of re-implementing the
`timedelta` conversion, so the shared logic stays in one place and remains
consistent.
- Around line 1305-1313: Replace the private pandas call in
_get_bars_frame_window with the public slicing API, since
df_source._slice(slice(start_row, end_row)) is the centralized fast-path used by
get_bars() and depends on an unstable internal method. Update the slice
operation in _get_bars_frame_window to use the equivalent public indexer on
df_source, keeping the same start_row/end_row behavior and return contract so
the three fast paths continue to work unchanged.

In `@lumibot/tools/databento_helper_polars.py`:
- Around line 580-585: The duplicate-timestamp cleanup in the
df.index.has_duplicates branch silently drops rows, so add a logger.warning or
logger.debug before deduping to report how many duplicate ts_event rows were
found and that keep="last" is being applied. Use the existing helper around the
index-normalization logic in databento_helper_polars.py so the behavior stays
the same while making the data issue visible.
🪄 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: 36c0cd4a-6fdb-4984-880d-e5f5620922ab

📥 Commits

Reviewing files that changed from the base of the PR and between 4f02694 and fc5e4c7.

📒 Files selected for processing (15)
  • .github/workflows/alpaca-live-broker.yml
  • CHANGELOG.md
  • docs/ALPACA_LIVE_BROKER_CI.md
  • docsrc/environment_variables.rst
  • lumibot/backtesting/alpaca_backtesting.py
  • lumibot/entities/data.py
  • lumibot/strategies/strategy_executor.py
  • lumibot/tools/databento_helper.py
  • lumibot/tools/databento_helper_polars.py
  • tests/test_agent_alpaca_news_live_apitest.py
  • tests/test_data_entity.py
  • tests/test_databento_helper.py
  • tests/test_ibkr_futures_daily_series.py
  • tests/test_memory_efficiency_entities_backtesting.py
  • tests/test_strategy_executor_order_index.py
✅ Files skipped from review due to trivial changes (4)
  • lumibot/tools/databento_helper.py
  • tests/test_strategy_executor_order_index.py
  • CHANGELOG.md
  • tests/test_ibkr_futures_daily_series.py

Comment thread lumibot/entities/data.py Outdated
Comment on lines +1239 to +1313
@check_data
def _validate_bars_request(self, dt, length=1, timestep=None, timeshift=0):
"""Run the standard data availability checks without materializing bar data."""
return True

def _normalize_timeshift_to_rows(self, timeshift):
if timeshift is None:
return 0

if isinstance(timeshift, datetime.timedelta):
if self.timestep == "day":
return int(timeshift.total_seconds() / (24 * 3600))
if self.timestep == "hour":
return int(timeshift.total_seconds() / 3600)
return int(timeshift.total_seconds() / 60)

return int(timeshift or 0)

def _get_bars_row_bounds(self, dt, length=1, timeshift=0):
"""Return integer row bounds matching `_get_bars_dict()` slice semantics."""
timeshift = self._normalize_timeshift_to_rows(timeshift)

iter_count = self.get_iter_count(dt)
try:
if pd.isna(iter_count):
iter_count = 0
except Exception:
pass

# `_get_bars_dict()` slices with `end_row` as an exclusive bound. Daily bars are already
# complete for intraday requests, so include the as-of daily row.
if self.timestep == "day":
end_row = int(iter_count) + 1 - timeshift
else:
end_row = int(iter_count) - timeshift

data_len = getattr(self, "_data_len", None)
if data_len is None:
data_len = len(next(iter(self.datalines.values())).dataline) if self.datalines else len(self.df.index)
self._data_len = int(data_len)

end_row = max(0, min(end_row, int(data_len)))
start_row = max(0, end_row - int(length))
if start_row > end_row:
start_row = end_row
if start_row == end_row and end_row > 0:
start_row = max(0, end_row - 1)

return int(start_row), int(end_row), int(timeshift)

def _get_bars_source_frame(self):
"""Return the cached OHLCV/corporate-action frame used by `get_bars()`."""
df_source = getattr(self, "_bars_df", None)
if df_source is not None:
return df_source

try:
bars_cols = getattr(self, "_bars_cols", None)
df_source = self.df[bars_cols].copy(deep=False) if bars_cols else self.df
if bars_cols:
self._bars_df = df_source
except Exception:
df_source = self.df

return df_source

def _get_bars_frame_window(self, dt, length=1, timeshift=0):
"""Slice the cached bars frame without building an intermediate dataline DataFrame."""
start_row, end_row, normalized_timeshift = self._get_bars_row_bounds(dt, length=length, timeshift=timeshift)
df_source = self._get_bars_source_frame()
df = df_source._slice(slice(start_row, end_row))
if df is None or df.shape[0] == 0:
return None, start_row, end_row, normalized_timeshift
return df, start_row, end_row, normalized_timeshift

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant symbol ranges first.
ast-grep outline lumibot/entities/data.py --view expanded > /tmp/data_outline.txt
printf '%s\n' '--- AST outline (filtered) ---'
sed -n '1,220p' /tmp/data_outline.txt

printf '\n%s\n' '--- Relevant source excerpt around lines 1230-1575 ---'
sed -n '1230,1575p' lumibot/entities/data.py | cat -n

printf '\n%s\n' '--- Search for strict-end/stale-bar logic and fast-path callers ---'
rg -n "_validate_bars_request|strict_end_check|_strict_intraday_stale_bar_error|_get_bars_frame_window|_get_bars_row_bounds|native timestep|native.*1" lumibot/entities/data.py

printf '\n%s\n' '--- Search tests mentioning strict_end_check / stale bars / native minute paths ---'
rg -n "strict_end_check|stale_bar|native.*minute|bucket boundary|_native_timestep_quantity|get_bars" tests

Repository: Lumiwealth/lumibot

Length of output: 32889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

path = Path("lumibot/entities/data.py")
lines = path.read_text().splitlines()

def find_block(start_marker, end_marker=None):
    start = next(i for i,l in enumerate(lines,1) if start_marker in l)
    if end_marker:
        end = next(i for i,l in enumerate(lines[start:], start) if end_marker in l)
    else:
        end = min(len(lines), start + 220)
    return start, end

# Inspect the get_bars method around the native fast paths.
start, end = find_block("def get_bars(", "def get_bars_between_dates(")
segment = "\n".join(f"{i:4d}: {lines[i-1]}" for i in range(start, end+1))

checks = {
    "_validate_bars_request calls in get_bars body": len(re.findall(r"_validate_bars_request\(", segment)),
    "strict_end_check mentions in get_bars body": len(re.findall(r"strict_end_check", segment)),
    "_strict_intraday_stale_bar_error mentions in get_bars body": len(re.findall(r"_strict_intraday_stale_bar_error", segment)),
    "_get_bars_frame_window mentions in get_bars body": len(re.findall(r"_get_bars_frame_window", segment)),
}

print("--- get_bars body scan ---")
for k, v in checks.items():
    print(f"{k}: {v}")

print("\n--- matching lines near fast paths ---")
for needle in ["_validate_bars_request(", "_strict_intraday_stale_bar_error", "strict_end_check", "_get_bars_frame_window("]:
    print(f"\n{needle}:")
    for i,l in enumerate(lines[start-1:end], start):
        if needle in l:
            print(f"{i}: {l}")

print("\n--- tests containing strict_end_check near get_bars calls ---")
for test in ["tests/test_data_entity.py", "tests/test_hour_timestep_support.py"]:
    p = Path(test)
    if not p.exists():
        continue
    tlines = p.read_text().splitlines()
    for i,l in enumerate(tlines,1):
        if "strict_end_check" in l or "get_bars(" in l and "5m" in l:
            print(f"{test}:{i}: {l}")
PY

Repository: Lumiwealth/lumibot

Length of output: 1438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '700,950p' lumibot/entities/data.py | cat -n

printf '\n%s\n' '--- get_bars method around native fast paths ---'
sed -n '1223,1568p' lumibot/entities/data.py | cat -n

printf '\n%s\n' '--- strict_end_check tests around the cited cases ---'
sed -n '150,340p' tests/test_data_entity.py | cat -n

Repository: Lumiwealth/lumibot

Length of output: 41505


Call the strict availability check before the native slice fast paths. Both native branches in Data.get_bars() jump straight to _slice(), so strict_end_check, _strict_intraday_stale_bar_error, and the usual out-of-range ValueError are skipped there. A strict caller can get stale bars back from the fast path instead of the same error/tolerance handling used on the resample path.

🤖 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 1239 - 1313, The native slice fast
paths in Data.get_bars() are bypassing the strict availability checks, so
strict_end_check, _strict_intraday_stale_bar_error, and the usual out-of-range
ValueError handling never run. Update the native branches to invoke the same
strict validation used by the resample path before calling _slice(), likely by
reusing _validate_bars_request() and the row-bound helpers such as
_get_bars_row_bounds()/_get_bars_frame_window(). Keep the fast path, but ensure
strict callers receive the same stale-bar and bounds behavior as non-native
requests.

@mpelteshki

Copy link
Copy Markdown
Collaborator Author

Superseded by #1127, which now contains the useful Alpaca strategy quote, bars, option-chain, and paper-order lifecycle coverage in the required primary CI gate. The duplicate workflow and unrelated branch churn were intentionally not carried over.

@mpelteshki mpelteshki closed this Jul 13, 2026
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