Add date/time range support to get_historical_candles

- _fetch_candles now accepts explicit from_ts/to_ts timestamps; falls
  back to count-based window when omitted
- get_historical_candles accepts from_date/to_date in YYYY-MM-DD,
  YYYY-MM-DD HH:MM, or YYYY-MM-DD HH:MM:SS formats (all UTC)
- Yahoo Finance fallback switched from range= to period1/period2 so
  it respects the exact window in both modes
- Added 11 tests covering date range, datetime, end-of-day bump,
  invalid format, count cap, and pattern detection toggle
- gitignore uv.lock (Dockerfile uses pip, not uv sync)
This commit is contained in:
Achmad
2026-05-17 08:10:03 +00:00
parent b24a424c8f
commit 79d1e0e538
3 changed files with 137 additions and 20 deletions
+91
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import calendar
import os
import time
from unittest.mock import patch
import pytest
@@ -15,6 +17,7 @@ from server import (
find_top_gainers,
find_top_losers,
fundamental_scan,
get_historical_candles,
get_stock_quotes,
list_fields,
list_markets,
@@ -468,3 +471,91 @@ class TestSessionTools:
server._session_cookies = None
result = clear_session()
assert "cleared" in result
# ---------------------------------------------------------------------------
# get_historical_candles
# ---------------------------------------------------------------------------
_MOCK_CANDLES = [
{"time": 1746662400, "open": 100.0, "high": 110.0, "low": 95.0, "close": 105.0, "volume": 1_000_000},
{"time": 1746748800, "open": 105.0, "high": 115.0, "low": 100.0, "close": 108.0, "volume": 1_200_000},
{"time": 1746835200, "open": 108.0, "high": 112.0, "low": 104.0, "close": 106.0, "volume": 900_000},
]
class TestGetHistoricalCandles:
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_basic_count_based(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL", resolution="D", count=3)
mock_fetch.assert_called_once_with("NASDAQ:AAPL", "D", 3, from_ts=None, to_ts=None)
assert "NASDAQ:AAPL" in result
assert "3 candles" in result
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_from_date_only(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL", from_date="2026-05-08")
kw = mock_fetch.call_args.kwargs
expected_from = int(calendar.timegm(time.strptime("2026-05-08", "%Y-%m-%d")))
assert kw["from_ts"] == expected_from
assert kw["to_ts"] is not None # defaults to now
assert "2026-05-08" in result
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_from_and_to_date(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL", from_date="2026-05-08", to_date="2026-05-13")
kw = mock_fetch.call_args.kwargs
expected_from = int(calendar.timegm(time.strptime("2026-05-08", "%Y-%m-%d")))
expected_to = int(calendar.timegm(time.strptime("2026-05-13", "%Y-%m-%d"))) + 86399
assert kw["from_ts"] == expected_from
assert kw["to_ts"] == expected_to
assert "2026-05-08 → 2026-05-13" in result
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_datetime_strings(self, mock_fetch):
get_historical_candles("NASDAQ:AAPL", from_date="2026-05-17 14:00", to_date="2026-05-17 14:05")
kw = mock_fetch.call_args.kwargs
expected_from = int(calendar.timegm(time.strptime("2026-05-17 14:00", "%Y-%m-%d %H:%M")))
expected_to = int(calendar.timegm(time.strptime("2026-05-17 14:05", "%Y-%m-%d %H:%M")))
assert kw["from_ts"] == expected_from
assert kw["to_ts"] == expected_to
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_datetime_with_seconds(self, mock_fetch):
get_historical_candles("NASDAQ:AAPL", from_date="2026-05-17 14:00:30", to_date="2026-05-17 14:05:00")
kw = mock_fetch.call_args.kwargs
expected_from = int(calendar.timegm(time.strptime("2026-05-17 14:00:30", "%Y-%m-%d %H:%M:%S")))
assert kw["from_ts"] == expected_from
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_date_only_to_date_end_of_day(self, mock_fetch):
# date-only to_date should be bumped to end of that day (23:59:59)
get_historical_candles("NASDAQ:AAPL", from_date="2026-05-08", to_date="2026-05-13")
kw = mock_fetch.call_args.kwargs
midnight = int(calendar.timegm(time.strptime("2026-05-13", "%Y-%m-%d")))
assert kw["to_ts"] == midnight + 86399
@patch("server._fetch_candles", return_value=[])
def test_no_data_returned(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL")
assert "No historical data" in result
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_invalid_date_format_raises(self, mock_fetch):
with pytest.raises(ValueError, match="Unrecognised date"):
get_historical_candles("NASDAQ:AAPL", from_date="05/08/2026")
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_count_capped_at_500(self, mock_fetch):
get_historical_candles("NASDAQ:AAPL", count=9999)
assert mock_fetch.call_args.args[2] == 500
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_detect_patterns_included_by_default(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL")
assert "Pattern Detection" in result
@patch("server._fetch_candles", return_value=_MOCK_CANDLES)
def test_detect_patterns_disabled(self, mock_fetch):
result = get_historical_candles("NASDAQ:AAPL", detect_patterns=False)
assert "Pattern Detection" not in result