Files
fxhnt/tests/integration/test_tiingo_daily.py
2026-06-16 08:50:25 +02:00

135 lines
4.8 KiB
Python

"""TiingoDailyClient — native total-return adjClose (equity/ETF) + crypto routing. NO live network:
the `_get` HTTP layer is monkeypatched to return fixed fixtures.
Invariants proven here:
(1) equity symbols hit the daily endpoint and read `adjClose` (NOT raw `close`).
(2) crypto `BTC-USD` is routed to the crypto endpoint, ticker mapped to `btcusd`, and the nested
`priceData[].close` is read (crypto has no adjClose).
(3) the request window's startDate is computed from `lookback_days` (today - lookback_days).
"""
from __future__ import annotations
import datetime as dt
from typing import Any
from fxhnt.adapters.data.tiingo_daily import TiingoDailyClient
def _daily(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Tiingo equity/ETF daily response: a flat list with both `close` and `adjClose`."""
return [
{
"date": f"{r['date']}T00:00:00.000Z",
"close": r["close"],
"adjClose": r["adjClose"],
"divCash": 0.0,
"splitFactor": 1.0,
}
for r in rows
]
def _crypto(ticker: str, rows: list[tuple[str, float]]) -> list[dict[str, Any]]:
"""Tiingo crypto response: a one-element list with nested `priceData` (close only, no adjClose)."""
return [
{
"ticker": ticker,
"baseCurrency": "btc",
"quoteCurrency": "usd",
"priceData": [
{"date": f"{d}T00:00:00.000Z", "open": c, "high": c, "low": c, "close": c, "volume": 1.0}
for d, c in rows
],
}
]
def test_equity_uses_adjclose_not_close() -> None:
client = TiingoDailyClient(api_key="x")
# adjClose deliberately differs from close so we can prove which field is read.
rows = [
{"date": "2026-01-02", "close": 100.0, "adjClose": 90.0},
{"date": "2026-01-03", "close": 101.0, "adjClose": 91.5},
{"date": "2026-01-06", "close": 102.0, "adjClose": 93.0},
]
captured: dict[str, str] = {}
def fake_get(url: str, tries: int = 4) -> Any:
captured["url"] = url
assert "/tiingo/daily/SPY/prices" in url
return _daily(rows)
client._get = fake_get
out = client.adj_closes("SPY")
assert out == {"2026-01-02": 90.0, "2026-01-03": 91.5, "2026-01-06": 93.0}
assert all(isinstance(k, str) and isinstance(v, float) for k, v in out.items())
# Daily endpoint, NOT crypto.
assert "/tiingo/crypto/" not in captured["url"]
def test_crypto_btc_routed_and_ticker_mapped() -> None:
client = TiingoDailyClient(api_key="x")
rows = [("2026-01-02", 45000.0), ("2026-01-03", 46000.5), ("2026-01-06", 44000.25)]
captured: dict[str, str] = {}
def fake_get(url: str, tries: int = 4) -> Any:
captured["url"] = url
# Routed to the crypto endpoint with the Yahoo ticker mapped to btcusd.
assert "/tiingo/crypto/prices" in url
assert "tickers=btcusd" in url
assert "resampleFreq=1day" in url
assert "/tiingo/daily/" not in url
return _crypto("btcusd", rows)
client._get = fake_get
out = client.adj_closes("BTC-USD")
# Nested priceData[].close is read (crypto has no adjClose).
assert out == {"2026-01-02": 45000.0, "2026-01-03": 46000.5, "2026-01-06": 44000.25}
assert "tickers=btcusd" in captured["url"]
def test_startdate_window_from_lookback_days() -> None:
client = TiingoDailyClient(api_key="x", lookback_days=1100)
captured: dict[str, str] = {}
def fake_get(url: str, tries: int = 4) -> Any:
captured["url"] = url
return _daily([{"date": "2026-01-02", "close": 100.0, "adjClose": 100.0}])
client._get = fake_get
client.adj_closes("SPY")
expected_start = (dt.date.today() - dt.timedelta(days=1100)).isoformat()
expected_end = dt.date.today().isoformat()
assert f"startDate={expected_start}" in captured["url"]
assert f"endDate={expected_end}" in captured["url"]
def test_crypto_window_from_lookback_days() -> None:
client = TiingoDailyClient(api_key="x", lookback_days=365)
captured: dict[str, str] = {}
def fake_get(url: str, tries: int = 4) -> Any:
captured["url"] = url
return _crypto("btcusd", [("2026-01-02", 45000.0)])
client._get = fake_get
client.adj_closes("BTC-USD")
expected_start = (dt.date.today() - dt.timedelta(days=365)).isoformat()
assert f"startDate={expected_start}" in captured["url"]
def test_empty_equity_response_returns_empty_dict() -> None:
client = TiingoDailyClient(api_key="x")
client._get = lambda url, tries=4: [] # noqa: E731
assert client.adj_closes("SPY") == {}
def test_empty_crypto_response_returns_empty_dict() -> None:
client = TiingoDailyClient(api_key="x")
client._get = lambda url, tries=4: [] # noqa: E731
assert client.adj_closes("BTC-USD") == {}