0ba5a05219
Add a unified historical endpoint that fans out to every exchange supporting the requested (asset_class, symbol) pair, then merges the results into a single consensus candle series with per-bar divergence metrics: - candles[i].close = median across sources - candles[i].sources = count of contributing exchanges - candles[i].div_pct = (max-min)/median for that bar's close Crypto routes BTC/ETH/SOL across Bybit + Hyperliquid + Deribit; equities route to Alpaca for now (IBKR omitted from MVP because its bars endpoint takes a relative period instead of start/end). Partial failures return a warning envelope (failed_sources) instead of failing the whole request; all sources failing → HTTP 502. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 lines
1.4 KiB
Python
38 lines
1.4 KiB
Python
"""Pure consensus aggregation: merge per-source OHLCV candles by timestamp.
|
|
|
|
The output is a single time-series with the median OHLC across sources,
|
|
mean volume, the contributing source count, and a divergence % computed
|
|
on the close range. div_pct gives a quick quality signal: 0 means full
|
|
agreement, > X% means at least one source is suspect.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from statistics import median
|
|
from typing import Any
|
|
|
|
|
|
def merge_candles(by_source: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]:
|
|
grouped: dict[int, list[dict[str, Any]]] = defaultdict(list)
|
|
for candles in by_source.values():
|
|
for c in candles:
|
|
grouped[int(c["timestamp"])].append(c)
|
|
|
|
out: list[dict[str, Any]] = []
|
|
for ts in sorted(grouped):
|
|
rows = grouped[ts]
|
|
closes = [float(r["close"]) for r in rows]
|
|
med_close = float(median(closes))
|
|
div_pct = (max(closes) - min(closes)) / med_close if med_close else 0.0
|
|
out.append({
|
|
"timestamp": ts,
|
|
"open": float(median(float(r["open"]) for r in rows)),
|
|
"high": float(median(float(r["high"]) for r in rows)),
|
|
"low": float(median(float(r["low"]) for r in rows)),
|
|
"close": med_close,
|
|
"volume": sum(float(r["volume"]) for r in rows) / len(rows),
|
|
"sources": len(rows),
|
|
"div_pct": div_pct,
|
|
})
|
|
return out
|