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