options bars: split a symbol batch in half on HTTP 504

SNDK's October-2026 expiry month (1000 contracts, 3.5 months of daily
bars) timed out on every retry, so the backfill skipped it twice. A 504 on
a request for 50+ symbols now restarts that chunk as two halves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 12:31:50 -07:00
parent 1b17a9cd1b
commit 251014d3b2

View File

@@ -106,6 +106,10 @@ OCC = re.compile(r"^[A-Z]{1,5}\d{6,7}[CP]\d{8}$") # what the bars endpoint acc
BAR_COLS = ["date", "underlying", "symbol", "type", "expiry", "strike", "o", "h", "l", "c", "v", "n", "vw"] BAR_COLS = ["date", "underlying", "symbol", "type", "expiry", "strike", "o", "h", "l", "c", "v", "n", "vw"]
class BackendTimeout(RuntimeError):
"""HTTP 504 on a request for many symbols: the caller splits the batch."""
class Api: class Api:
"""Paced GET with retries. ``yield_to_snapshot`` sleeps through 16:10-16:55 """Paced GET with retries. ``yield_to_snapshot`` sleeps through 16:10-16:55
New York on weekdays so a backfill leaves the rate limit to the daily run.""" New York on weekdays so a backfill leaves the rate limit to the daily run."""
@@ -140,6 +144,8 @@ class Api:
time.sleep(2 ** attempt) time.sleep(2 ** attempt)
continue continue
self.n += 1 self.n += 1
if r.status_code == 504 and params.get("symbols", "").count(",") >= 50:
raise BackendTimeout(r.text[:200]) # the same batch times out again (SNDK, 1000 contracts)
if r.status_code == 429 or r.status_code >= 500: if r.status_code == 429 or r.status_code >= 500:
time.sleep(2 ** attempt) time.sleep(2 ** attempt)
continue continue
@@ -238,14 +244,23 @@ def option_bars(api: Api, symbols: list[str], start: str, end: str, timeframe: s
e = (dt.datetime.combine(dt.date.fromisoformat(end), dt.time(0), NY) if len(end) == 10 e = (dt.datetime.combine(dt.date.fromisoformat(end), dt.time(0), NY) if len(end) == 10
else dt.datetime.fromisoformat(end)) else dt.datetime.fromisoformat(end))
end = min(e, dt.datetime.now(NY) - dt.timedelta(minutes=16)).isoformat() end = min(e, dt.datetime.now(NY) - dt.timedelta(minutes=16)).isoformat()
def fetch(chunk: list[str]) -> list[dict]:
params = {"symbols": ",".join(chunk), "timeframe": timeframe, "start": start, "end": end, "limit": 10000}
got = []
try:
for bars in api.pages(f"{DATA}/v1beta1/options/bars", params, "bars"):
for sym, bl in bars.items():
got += [{"symbol": sym, "t": b["t"], "o": b["o"], "h": b["h"], "l": b["l"], "c": b["c"],
"v": b["v"], "n": b.get("n"), "vw": b.get("vw")} for b in bl]
except BackendTimeout: # restart the chunk as two halves
h = len(chunk) // 2
return fetch(chunk[:h]) + fetch(chunk[h:])
return got
rows = [] rows = []
for i in range(0, len(symbols), 1000): for i in range(0, len(symbols), 1000):
params = {"symbols": ",".join(symbols[i:i + 1000]), "timeframe": timeframe, "start": start, "end": end, rows += fetch(symbols[i:i + 1000])
"limit": 10000}
for bars in api.pages(f"{DATA}/v1beta1/options/bars", params, "bars"):
for sym, bl in bars.items():
rows += [{"symbol": sym, "t": b["t"], "o": b["o"], "h": b["h"], "l": b["l"], "c": b["c"],
"v": b["v"], "n": b.get("n"), "vw": b.get("vw")} for b in bl]
return rows return rows