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
+83 -26
View File
@@ -44,6 +44,53 @@ RESOLUTION_MAP = {
"1d": "1D",
}
# Deribit's get_tradingview_chart_data returns at most ~5000 candles per call and,
# when the range needs more, silently drops the oldest and keeps the newest slice.
# We page through wide ranges in windows sized to this cap so start_date is honored.
_MAX_CANDLES_PER_REQUEST = 5000
# Hard stop on pagination so a tiny resolution over a huge span can't loop forever.
_MAX_HISTORY_PAGES = 500
# Candle interval in milliseconds, keyed by the Deribit resolution code.
_RESOLUTION_MS = {
"1": 60_000,
"3": 180_000,
"5": 300_000,
"10": 600_000,
"15": 900_000,
"30": 1_800_000,
"60": 3_600_000,
"120": 7_200_000,
"180": 10_800_000,
"360": 21_600_000,
"720": 43_200_000,
"1D": 86_400_000,
}
def _date_to_ms(date_str: str, *, boundary: str) -> int:
"""Parse a date/datetime string to an epoch-millisecond UTC timestamp.
Bare dates (YYYY-MM-DD) are anchored in UTC: a ``start`` boundary maps to
00:00:00 of that day, an ``end`` boundary to 23:59:59.999 so the whole day's
intraday candles are included (asking end_date=today returns data up to now).
Strings carrying an explicit time are honored as given (naive values are UTC).
"""
import datetime as _dt
s = date_str.strip()
has_time = "t" in s.lower() or " " in s
try:
dt = _dt.datetime.fromisoformat(s)
except ValueError:
dt = _dt.datetime.strptime(s, "%Y-%m-%d")
if dt.tzinfo is None:
dt = dt.replace(tzinfo=_dt.UTC)
if boundary == "end" and not has_time:
dt = dt.replace(hour=23, minute=59, second=59, microsecond=999_000)
return int(dt.timestamp() * 1000)
class DeribitAuthError(Exception):
"""Deribit auth failed (bad credentials, missing scope, env mismatch)."""
@@ -397,19 +444,39 @@ class DeribitClient:
async def get_historical(
self, instrument: str, start_date: str, end_date: str, resolution: str
) -> dict:
# Convert ISO date strings to millisecond timestamps
import datetime as _dt
def _to_ms(date_str: str) -> int:
try:
dt = _dt.datetime.fromisoformat(date_str)
except ValueError:
dt = _dt.datetime.strptime(date_str, "%Y-%m-%d")
return int(dt.timestamp() * 1000)
start_ms = _to_ms(start_date)
end_ms = _to_ms(end_date)
start_ms = _date_to_ms(start_date, boundary="start")
end_ms = _date_to_ms(end_date, boundary="end")
res = RESOLUTION_MAP.get(resolution, resolution)
interval_ms = _RESOLUTION_MS.get(res)
by_ts: dict[int, dict] = {}
if interval_ms and end_ms >= start_ms:
# Page the range in fixed windows of <= _MAX_CANDLES_PER_REQUEST so
# Deribit's per-call cap can't silently truncate older candles.
window_span = interval_ms * _MAX_CANDLES_PER_REQUEST
cursor = start_ms
pages = 0
while cursor <= end_ms and pages < _MAX_HISTORY_PAGES:
window_end = min(cursor + window_span - interval_ms, end_ms)
for c in await self._fetch_chart_window(
instrument, cursor, window_end, res
):
if start_ms <= c["timestamp"] <= end_ms:
by_ts[c["timestamp"]] = c
pages += 1
if window_end >= end_ms:
break
cursor = window_end + interval_ms
else:
for c in await self._fetch_chart_window(instrument, start_ms, end_ms, res):
by_ts[c["timestamp"]] = c
# validate_candles sorts by timestamp, so insertion order is irrelevant.
return {"candles": validate_candles(list(by_ts.values()))}
async def _fetch_chart_window(
self, instrument: str, start_ms: int, end_ms: int, res: str
) -> list[dict]:
raw = await self._request(
"public/get_tradingview_chart_data",
{
@@ -427,7 +494,7 @@ class DeribitClient:
closes = r.get("close", []) or []
volumes = r.get("volume", []) or []
n = min(len(ticks), len(opens), len(highs), len(lows), len(closes), len(volumes))
candles = validate_candles([
return [
{
"timestamp": ticks[i],
"open": opens[i],
@@ -437,8 +504,7 @@ class DeribitClient:
"volume": volumes[i],
}
for i in range(n)
])
return {"candles": candles}
]
async def get_dvol(
self,
@@ -447,17 +513,8 @@ class DeribitClient:
end_date: str,
resolution: str = "1D",
) -> dict:
import datetime as _dt
def _to_ms(date_str: str) -> int:
try:
dt = _dt.datetime.fromisoformat(date_str)
except ValueError:
dt = _dt.datetime.strptime(date_str, "%Y-%m-%d")
return int(dt.timestamp() * 1000)
start_ms = _to_ms(start_date)
end_ms = _to_ms(end_date)
start_ms = _date_to_ms(start_date, boundary="start")
end_ms = _date_to_ms(end_date, boundary="end")
res = RESOLUTION_MAP.get(resolution, resolution)
raw = await self._request(
"public/get_volatility_index_data",