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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user