Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion application/runtime_broker_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from zoneinfo import ZoneInfo

import pandas as pd

Expand Down Expand Up @@ -39,6 +40,14 @@ def _utcnow() -> datetime:
return datetime.now(timezone.utc)


_NEW_YORK_TZ = ZoneInfo("America/New_York")


def _market_date(value: datetime) -> date:
normalized = value if value.tzinfo is not None else value.replace(tzinfo=timezone.utc)
return normalized.astimezone(_NEW_YORK_TZ).date()


@dataclass(frozen=True)
class FirstradeBrokerAdapters:
client: FirstradeBrokerClient
Expand Down Expand Up @@ -101,6 +110,18 @@ def load_price_series(symbol: str) -> PriceSeries:
)
if not points:
raise ValueError(f"Firstrade OHLC did not return price history for {normalized}.")
try:
quote = load_quote(normalized)
except Exception:
quote = None
if quote is not None and quote.last_price > 0:
quote_point = PricePoint(as_of=quote.as_of, close=quote.last_price)
last_market_date = _market_date(points[-1].as_of)
quote_market_date = _market_date(quote_point.as_of)
if quote_market_date > last_market_date:
points.append(quote_point)
Comment on lines +118 to +122

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 Avoid dating stale quotes as new daily bars

When get_price_series() runs on a weekend/holiday or after midnight ET before a new OHLC candle exists, load_quote() stamps the quote with self.clock() rather than the trade timestamp, so this appends an artificial market-history point for a non-trading day. The client already exposes quote_time/last_trade_time in get_quote() (application/firstrade_client.py), but they are ignored here; strategies consuming market_history/derived indicators will see an extra daily bar even though the last trade was on the previous market session.

Useful? React with 👍 / 👎.

elif quote_market_date == last_market_date:
points[-1] = quote_point
series = PriceSeries(symbol=normalized, currency="USD", points=tuple(points))
series_cache[normalized] = series
return series
Expand Down
89 changes: 87 additions & 2 deletions tests/test_runtime_broker_adapters.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
from __future__ import annotations

from datetime import datetime, timezone

from application.runtime_broker_adapters import build_runtime_broker_adapters


def _timestamp_ms(value: datetime) -> int:
return int(value.timestamp() * 1000)


class FakeClient:
def __init__(self, *, quote_payload=None, quote_error: Exception | None = None, ohlc=None):
self.quote_payload = quote_payload or {"last": "10.50", "bid": "10.40", "ask": "10.60"}
self.quote_error = quote_error
self.ohlc = ohlc or [(1700000000000, 9, 11, 8, 10, 1000)]

def get_quote(self, _account, symbol):
return {"symbol": symbol, "last": "10.50", "bid": "10.40", "ask": "10.60"}
if self.quote_error is not None:
raise self.quote_error
return {"symbol": symbol, **self.quote_payload}

def get_ohlc(self, _symbol, _range):
return [(1700000000000, 9, 11, 8, 10, 1000)]
return self.ohlc

def get_balances(self, _account):
return {"total_value": "120.00", "cash": "20.00", "buying_power": "30.00"}
Expand All @@ -33,3 +46,75 @@ def test_runtime_adapters_build_quote_and_portfolio_ports():
assert portfolio.total_equity == 120.0
assert portfolio.cash_balance == 20.0
assert portfolio.positions[0].symbol == "SPY"


def test_price_series_appends_live_quote_when_history_lags_today():
adapters = build_runtime_broker_adapters(
client=FakeClient(
quote_payload={"last": "12.00", "bid": "11.90", "ask": "12.10"},
ohlc=[
(
_timestamp_ms(datetime(2026, 5, 26, 4, tzinfo=timezone.utc)),
9,
11,
8,
10,
1000,
)
],
),
account="12345678",
clock=lambda: datetime(2026, 5, 27, 19, 45, tzinfo=timezone.utc),
)

series = adapters.build_market_data_port().get_price_series("SPY")

assert [point.close for point in series.points] == [10.0, 12.0]


def test_price_series_replaces_same_day_history_with_live_quote():
adapters = build_runtime_broker_adapters(
client=FakeClient(
quote_payload={"last": "12.00", "bid": "11.90", "ask": "12.10"},
ohlc=[
(
_timestamp_ms(datetime(2026, 5, 27, 4, tzinfo=timezone.utc)),
9,
11,
8,
10,
1000,
)
],
),
account="12345678",
clock=lambda: datetime(2026, 5, 27, 19, 45, tzinfo=timezone.utc),
)

series = adapters.build_market_data_port().get_price_series("SPY")

assert [point.close for point in series.points] == [12.0]


def test_price_series_falls_back_to_history_when_quote_unavailable():
adapters = build_runtime_broker_adapters(
client=FakeClient(
quote_error=RuntimeError("quote unavailable"),
ohlc=[
(
_timestamp_ms(datetime(2026, 5, 26, 4, tzinfo=timezone.utc)),
9,
11,
8,
10,
1000,
)
],
),
account="12345678",
clock=lambda: datetime(2026, 5, 27, 19, 45, tzinfo=timezone.utc),
)

series = adapters.build_market_data_port().get_price_series("SPY")

assert [point.close for point in series.points] == [10.0]