From fc3be349c6f0f5d8e30e921570a36f4e299fc8b1 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 28 May 2026 04:26:09 +0800 Subject: [PATCH] Use live quotes in Firstrade price history --- application/runtime_broker_adapters.py | 23 ++++++- tests/test_runtime_broker_adapters.py | 89 +++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/application/runtime_broker_adapters.py b/application/runtime_broker_adapters.py index aec0878..fdfebd2 100644 --- a/application/runtime_broker_adapters.py +++ b/application/runtime_broker_adapters.py @@ -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 @@ -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 @@ -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) + 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 diff --git a/tests/test_runtime_broker_adapters.py b/tests/test_runtime_broker_adapters.py index ff5478d..cc05ee1 100644 --- a/tests/test_runtime_broker_adapters.py +++ b/tests/test_runtime_broker_adapters.py @@ -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"} @@ -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]