fix(V2): Deribit get_historical end_date day-inclusive + pagination

end_date as a bare date now covers the whole UTC day (23:59:59.999)
instead of stopping at midnight, so end_date=today returns intraday up
to the last closed candle. Dates are parsed as explicit UTC and inline
timestamps (YYYY-MM-DDTHH:MM:SS) are honored for precise windows.

Wide ranges are paged in <=5000-candle windows so Deribit's per-call
cap can no longer silently drop start_date and the oldest candles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-28 14:25:55 +00:00
parent c5e48bf081
commit eb5f0148ad
3 changed files with 229 additions and 26 deletions
+134
View File
@@ -177,6 +177,140 @@ async def test_upstream_5xx_raises_clean_http_error(
assert "Deribit upstream" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_get_historical_end_date_is_day_inclusive(
httpx_mock: HTTPXMock, client: DeribitClient
):
"""Bare end_date covers the whole UTC day (23:59:59.999), not just midnight,
so 'end_date = today' returns intraday up to now instead of stopping at 00:00."""
import datetime as dt
start = dt.datetime(2026, 5, 20, tzinfo=dt.UTC)
start_ms = int(start.timestamp() * 1000)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_tradingview_chart_data"),
json={
"result": {
"ticks": [start_ms, start_ms + 3_600_000],
"open": [100.0, 100.5],
"high": [101.0, 101.5],
"low": [99.0, 99.5],
"close": [100.5, 101.0],
"volume": [10.0, 11.0],
}
},
)
result = await client.get_historical(
instrument="BTC-PERPETUAL",
start_date="2026-05-20",
end_date="2026-05-28",
resolution="1h",
)
assert len(result["candles"]) == 2
expected_start = start_ms
expected_end = int(
dt.datetime(2026, 5, 28, 23, 59, 59, 999_000, tzinfo=dt.UTC).timestamp()
* 1000
)
params = httpx_mock.get_requests()[0].url.params
assert int(params["start_timestamp"]) == expected_start
assert int(params["end_timestamp"]) == expected_end
@pytest.mark.asyncio
async def test_get_historical_explicit_time_is_honored(
httpx_mock: HTTPXMock, client: DeribitClient
):
"""An end_date carrying an explicit time is passed through as-is, not bumped
to end-of-day — clients can request precise intraday windows."""
import datetime as dt
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_tradingview_chart_data"),
json={"result": {"ticks": [], "open": [], "high": [], "low": [],
"close": [], "volume": []}},
)
await client.get_historical(
instrument="BTC-PERPETUAL",
start_date="2026-05-28T09:00:00",
end_date="2026-05-28T14:00:00",
resolution="1h",
)
params = httpx_mock.get_requests()[0].url.params
expected_end = int(
dt.datetime(2026, 5, 28, 14, 0, 0, tzinfo=dt.UTC).timestamp() * 1000
)
assert int(params["end_timestamp"]) == expected_end
@pytest.mark.asyncio
async def test_get_historical_paginates_beyond_cap(
httpx_mock: HTTPXMock, client: DeribitClient
):
"""A range wider than Deribit's ~5000-candle cap is paged window-by-window so
start_date is honored instead of being silently dropped."""
import datetime as dt
import httpx
interval = 3_600_000 # 1h
def chart_callback(request: httpx.Request) -> httpx.Response:
params = request.url.params
start = int(params["start_timestamp"])
end = int(params["end_timestamp"])
ticks = list(range(start, end + 1, interval))
# Mimic Deribit truncating to the most recent slice if a single call
# ever exceeds the cap (it shouldn't, because windows are sized to fit).
ticks = ticks[-5000:]
n = len(ticks)
return httpx.Response(
200,
json={
"result": {
"ticks": ticks,
"open": [100.0] * n,
"high": [101.0] * n,
"low": [99.0] * n,
"close": [100.5] * n,
"volume": [10.0] * n,
}
},
)
httpx_mock.add_callback(
chart_callback,
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_tradingview_chart_data"),
is_reusable=True,
)
result = await client.get_historical(
instrument="BTC-PERPETUAL",
start_date="2025-01-01",
end_date="2025-12-30",
resolution="1h",
)
candles = result["candles"]
# ~363 days * 24h ≈ 8712 candles → more than one ~5000 window.
assert len(candles) > 5000
assert len(httpx_mock.get_requests()) >= 2
expected_start = int(
dt.datetime(2025, 1, 1, tzinfo=dt.UTC).timestamp() * 1000
)
expected_end_bound = int(
dt.datetime(2025, 12, 30, 23, 59, 59, 999_000, tzinfo=dt.UTC).timestamp()
* 1000
)
assert candles[0]["timestamp"] == expected_start
assert candles[-1]["timestamp"] <= expected_end_bound
# No duplicate timestamps across window boundaries.
tss = [c["timestamp"] for c in candles]
assert len(tss) == len(set(tss))
assert tss == sorted(tss)
@pytest.mark.asyncio
async def test_private_call_with_bad_auth_returns_error_envelope(
httpx_mock: HTTPXMock, client: DeribitClient