From 20d6c368b5773f8ba4a9d54680c8f09783e04e5d Mon Sep 17 00:00:00 2001 From: leoca Date: Thu, 17 Sep 2026 01:21:44 +0200 Subject: [PATCH] fix: merge US stock 3m bars returned by the Yahoo chart The 3m timeframe requests 1m bars and relies on MERGE_FACTOR_MAP to group them, but only the yfinance fallback applied the merge. When the Yahoo chart endpoint answered, which is the normal case, get_kline returned raw one-minute bars labelled as 3m. --- .../app/data_sources/us_stock.py | 2 + .../tests/test_us_stock_intraday_window.py | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/backend_api_python/app/data_sources/us_stock.py b/backend_api_python/app/data_sources/us_stock.py index f1be0f7c0..61fa78cf8 100644 --- a/backend_api_python/app/data_sources/us_stock.py +++ b/backend_api_python/app/data_sources/us_stock.py @@ -505,6 +505,8 @@ def get_kline( klines = self._fetch_yahoo_chart(symbol, interval, start_date, end_date, effective_limit) + if klines and merge_factor > 1: + klines = self._merge_every_n_sorted_bars(klines, merge_factor) if not klines: if timeframe in ('1m', '3m', '5m', '15m', '30m', '1H', '4H'): # Nasdaq's intraday chart is a latest-session feed, not a diff --git a/backend_api_python/tests/test_us_stock_intraday_window.py b/backend_api_python/tests/test_us_stock_intraday_window.py index 9741ac788..6290d7f64 100644 --- a/backend_api_python/tests/test_us_stock_intraday_window.py +++ b/backend_api_python/tests/test_us_stock_intraday_window.py @@ -85,3 +85,47 @@ def history(self, **kwargs): assert captured["start"] == start assert captured["end"] == end + + +class _MinuteChartResponse: + def __init__(self, start_ts, count): + self._timestamps = [start_ts + 60 * i for i in range(count)] + + def raise_for_status(self): + return None + + def json(self): + n = len(self._timestamps) + return {"chart": {"result": [{ + "timestamp": self._timestamps, + "indicators": {"quote": [{ + "open": [100.0 + i for i in range(n)], + "high": [101.0 + i for i in range(n)], + "low": [99.0 + i for i in range(n)], + "close": [100.5 + i for i in range(n)], + "volume": [10] * n, + }]}, + }]}} + + +def test_three_minute_klines_from_yahoo_chart_are_merged(monkeypatch): + session_open = int(datetime(2026, 9, 14, 13, 30).timestamp()) + captured = {} + + def fake_get(_url, **kwargs): + captured.update(kwargs["params"]) + return _MinuteChartResponse(session_open, 6) + + monkeypatch.setattr(us_stock.requests, "get", fake_get) + source = USStockDataSource.__new__(USStockDataSource) + + bars = source.get_kline("NVDA", "3m", 2, before_time=session_open + 3600) + + assert captured["interval"] == "1m" + assert [bar["time"] for bar in bars] == [session_open, session_open + 180] + assert bars[0]["open"] == 100.0 + assert bars[0]["high"] == 103.0 + assert bars[0]["low"] == 99.0 + assert bars[0]["close"] == 102.5 + assert bars[0]["volume"] == 30 + assert bars[1]["close"] == 105.5