oanda: retry once on transient 401/5xx/network faults in candle fetch

The practice API rejects ~15-20 requests/day with a spurious 401
"Insufficient authorization" while the same token succeeds on adjacent
requests; each one silently cost a full scan cycle on that timeframe.
One retry after RETRY_DELAY_S (2 s) absorbs these plus transient
5xx/network faults. Real credential failures fail both attempts and
raise as before; other 4xx (bad request, 429) are never retried.

Tests: transient-401 recovery, persistent-401 raise, 400 not retried.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tujtmmhxj9mxKjPFSoVjmP
This commit is contained in:
root
2026-08-06 23:06:29 -04:00
parent b3022ef5c5
commit a42ef16113
2 changed files with 114 additions and 5 deletions
+31 -5
View File
@@ -8,6 +8,7 @@ Oanda's candle JSON, and exposes the real bid/ask spread from MBA candles.
"""
import logging
import time
from datetime import datetime
from typing import Dict, Optional
@@ -31,6 +32,7 @@ class OandaDataProvider(BaseDataProvider):
name = "oanda"
DATA_ERROR = OandaDataError
RETRY_DELAY_S = 2.0
OANDA_GRANULARITY = {
Timeframe.M1: "M1",
@@ -124,11 +126,35 @@ class OandaDataProvider(BaseDataProvider):
else:
params["count"] = min(count, self.MAX_BARS_PER_REQUEST)
try:
resp = self._session.get(url, params=params, timeout=30)
except requests.RequestException as e:
raise OandaDataError(
f"Oanda candle fetch failed for {instrument}: {e}") from e
# One retry absorbs Oanda's sporadic spurious 401s (~15-20/day on
# the practice API — the same token succeeds on adjacent requests)
# plus transient 5xx/network faults. A real credential problem
# fails both attempts and raises exactly as before. Other 4xx
# (bad request, 429) are never retried.
last_err: Optional[OandaDataError] = None
for attempt in range(2):
if attempt:
time.sleep(self.RETRY_DELAY_S)
try:
resp = self._session.get(url, params=params, timeout=30)
except requests.RequestException as e:
last_err = OandaDataError(
f"Oanda candle fetch failed for {instrument}: {e}")
last_err.__cause__ = e
continue
if resp.status_code == 401 or resp.status_code >= 500:
last_err = OandaDataError(
f"Oanda candle fetch failed for {instrument}: "
f"HTTP {resp.status_code}: {resp.text[:300]}")
if attempt == 0:
logger.warning(
f"Oanda fetch for {instrument} got HTTP "
f"{resp.status_code}; retrying in "
f"{self.RETRY_DELAY_S}s")
continue
break
else:
raise last_err
if resp.status_code != 200:
raise OandaDataError(