feat(V2): /mcp-cross/tools/get_historical with cross-exchange consensus

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>
This commit is contained in:
root
2026-05-10 21:41:18 +00:00
parent c94312d79f
commit 0ba5a05219
11 changed files with 580 additions and 0 deletions
@@ -0,0 +1,37 @@
"""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