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