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:
root
2026-05-10 19:19:20 +00:00
parent 110ca7f5cf
commit c94312d79f
8 changed files with 192 additions and 54 deletions
+13 -4
View File
@@ -20,6 +20,7 @@ from typing import Any
import httpx
from cerbero_mcp.common.candles import validate_candles
from cerbero_mcp.common.http import async_client
# ── Endpoint base ────────────────────────────────────────────────
@@ -301,9 +302,17 @@ class AlpacaClient:
bars_dict = (data or {}).get("bars") or {}
rows = bars_dict.get(symbol) or []
bars = [
def _iso_to_ms(ts: str | int | None) -> int | None:
if ts is None or isinstance(ts, int):
return ts
return int(_dt.datetime.fromisoformat(
ts.replace("Z", "+00:00")
).timestamp() * 1000)
candles = validate_candles([
{
"timestamp": b.get("t"),
"timestamp": _iso_to_ms(b.get("t")),
"open": b.get("o"),
"high": b.get("h"),
"low": b.get("l"),
@@ -311,12 +320,12 @@ class AlpacaClient:
"volume": b.get("v"),
}
for b in rows
]
])
return {
"symbol": symbol,
"asset_class": ac,
"interval": interval,
"bars": bars,
"candles": candles,
}
async def get_snapshot(self, symbol: str) -> dict: