feat(V2): shared Candle validator + uniform 'candles' response key
Introduce common/candles.py with a Pydantic Candle model enforcing OHLC consistency (high≥max, low≤min), non-negative volume and positive timestamp. validate_candles() coerces upstream rows, sorts by timestamp and raises HTTPException(502) on malformed data — surfacing upstream data corruption as a retryable envelope instead of silently returning nonsense. Wired into all five exchange historical endpoints (Bybit, Hyperliquid, Deribit, Alpaca, IBKR). BREAKING: Alpaca get_bars and IBKR get_bars now return 'candles' (was 'bars') to align with the other exchanges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,10 +11,11 @@ from fastapi import HTTPException
|
||||
from cerbero_mcp.common import indicators as ind
|
||||
from cerbero_mcp.common import microstructure as micro
|
||||
from cerbero_mcp.common import options as opt
|
||||
from cerbero_mcp.common.candles import validate_candles
|
||||
from cerbero_mcp.common.http import async_client
|
||||
|
||||
|
||||
def _parse_deribit_response(resp) -> dict:
|
||||
def _parse_deribit_response(resp: Any) -> dict[str, Any]:
|
||||
"""Map Deribit upstream errors to a clean HTTP 502 (retryable) instead of
|
||||
leaking JSONDecodeError when the body is HTML (e.g. Cloudflare 5xx page)."""
|
||||
if resp.status_code >= 500:
|
||||
@@ -23,7 +24,8 @@ def _parse_deribit_response(resp) -> dict:
|
||||
detail=f"Deribit upstream HTTP {resp.status_code}",
|
||||
)
|
||||
try:
|
||||
return resp.json()
|
||||
data: dict[str, Any] = resp.json()
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
@@ -121,10 +123,10 @@ class DeribitClient:
|
||||
resp = await http.get(url, params=request_params, headers=headers)
|
||||
data = _parse_deribit_response(resp)
|
||||
if "result" in data:
|
||||
return data # type: ignore[no-any-return]
|
||||
return data
|
||||
return {"result": None, "error": error_msg}
|
||||
|
||||
return data # type: ignore[no-any-return]
|
||||
return data
|
||||
|
||||
# ── Read tools ───────────────────────────────────────────────
|
||||
|
||||
@@ -418,24 +420,24 @@ class DeribitClient:
|
||||
},
|
||||
)
|
||||
r = raw.get("result") or {}
|
||||
candles = []
|
||||
ticks = r.get("ticks", []) or []
|
||||
opens = r.get("open", []) or []
|
||||
highs = r.get("high", []) or []
|
||||
lows = r.get("low", []) or []
|
||||
closes = r.get("close", []) or []
|
||||
volumes = r.get("volume", []) or []
|
||||
for idx, ts in enumerate(ticks):
|
||||
if idx >= min(len(opens), len(highs), len(lows), len(closes), len(volumes)):
|
||||
break
|
||||
candles.append({
|
||||
"timestamp": ts,
|
||||
"open": opens[idx],
|
||||
"high": highs[idx],
|
||||
"low": lows[idx],
|
||||
"close": closes[idx],
|
||||
"volume": volumes[idx],
|
||||
})
|
||||
n = min(len(ticks), len(opens), len(highs), len(lows), len(closes), len(volumes))
|
||||
candles = validate_candles([
|
||||
{
|
||||
"timestamp": ticks[i],
|
||||
"open": opens[i],
|
||||
"high": highs[i],
|
||||
"low": lows[i],
|
||||
"close": closes[i],
|
||||
"volume": volumes[i],
|
||||
}
|
||||
for i in range(n)
|
||||
])
|
||||
return {"candles": candles}
|
||||
|
||||
async def get_dvol(
|
||||
|
||||
Reference in New Issue
Block a user