refactor(V2): get_instruments common-only on /mcp (enriched with Deribit filters)

Move get_instruments off the per-exchange Deribit router and onto the
unified interface, without losing the advanced Deribit filtering.

- /mcp/tools/get_instruments now accepts the Deribit-specific filters
  (currency, kind, expiry_from/to, strike_min/max, min_open_interest) and
  pagination (offset/limit). Venues that list everything (Hyperliquid)
  ignore them. Response adds `meta: {deribit: {total, offset, limit,
  has_more}}` surfacing per-source pagination.
- Remove /mcp-deribit/tools/get_instruments (endpoint + tool wrapper +
  GetInstrumentsReq schema). The DeribitClient.get_instruments METHOD
  stays — it's what the unified normalizer calls.

Tests: cover the enriched filters + meta passthrough; app-boot asserts
/mcp-deribit/tools/get_instruments is gone. Docs (API_REFERENCE/README/
CLAUDE) and smoke updated; also fixed the stale Hyperliquid tool count
(14 → 15) found during the doc check.

325 passed, ruff clean. Verified live: kind=option limit=5 → 5/940
has_more=true; /mcp-deribit/tools/get_instruments → 404.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-05-29 11:06:09 +00:00
parent 6faf21369d
commit 8d7e2ca30f
10 changed files with 126 additions and 75 deletions
+6 -4
View File
@@ -61,11 +61,13 @@ Nota: nel modulo `sentiment` "bybit" resta come **fonte dati pubblica**
- **`/mcp` (comune)** — gli **unici** tool dati trasversali. `get_instruments` - **`/mcp` (comune)** — gli **unici** tool dati trasversali. `get_instruments`
(schema uniforme: `exchange`, `fees`, `history_start`, `type`, `tick_size`, (schema uniforme: `exchange`, `fees`, `history_start`, `type`, `tick_size`,
`native`); `get_historical` e `get_indicators` con `exchange` opzionale → `native`; filtri/paginazione Deribit + `meta` per-sorgente); `get_historical`
singolo venue se valorizzato, **consenso** multi-exchange (mediana OHLC + e `get_indicators` con `exchange` opzionale → singolo venue se valorizzato,
`div_pct`) se omesso. Supporta deribit + hyperliquid (IBKR non integrato). **consenso** multi-exchange (mediana OHLC + `div_pct`) se omesso. Supporta
deribit + hyperliquid (IBKR non integrato).
- **`/mcp-{exchange}`** — router per-exchange per il resto (ticker, ordini, - **`/mcp-{exchange}`** — router per-exchange per il resto (ticker, ordini,
options analytics, …). **NON** espongono più `get_historical`/`get_indicators`. options analytics, …). **NON** espongono più
`get_instruments`/`get_historical`/`get_indicators`.
Convenzione: `get_historical` ritorna la chiave uniforme Convenzione: `get_historical` ritorna la chiave uniforme
`candles: [{timestamp(ms), open, high, low, close, volume}]` (validatore in `candles: [{timestamp(ms), open, high, low, close, volume}]` (validatore in
+6 -3
View File
@@ -181,11 +181,12 @@ DVOL, GEX, P/C ratio, skew_25d, term_structure, iv_rank, realized_vol,
find_by_delta, calculate_spread_payoff, get_dealer_gamma_profile, find_by_delta, calculate_spread_payoff, get_dealer_gamma_profile,
get_vanna_charm, get_oi_weighted_skew, get_smile_asymmetry, get_vanna_charm, get_oi_weighted_skew, get_smile_asymmetry,
get_atm_vs_wings_vol, get_orderbook_imbalance, run_backtest, place_combo_order. get_atm_vs_wings_vol, get_orderbook_imbalance, run_backtest, place_combo_order.
(Storico/indicatori → `/mcp`.) (Strumenti/storico/indicatori → `/mcp`.)
### Hyperliquid ### Hyperliquid
Account summary, positions, orderbook, funding rate, basis spot/perp, Account summary, positions, orderbook, funding rate, basis spot/perp,
place_order, set_stop_loss, set_take_profit. (Storico/indicatori → `/mcp`.) place_order, set_stop_loss, set_take_profit.
(Strumenti/storico/indicatori → `/mcp`.)
### IBKR (Interactive Brokers) ### IBKR (Interactive Brokers)
Account, positions, activities, ticker, bars, snapshot, option chain, Account, positions, activities, ticker, bars, snapshot, option chain,
@@ -213,7 +214,9 @@ ancora integrato — usa `/mcp-ibkr`).
sé `exchange`, `fees` (maker/taker, live da Deribit, `null` dove l'exchange sé `exchange`, `fees` (maker/taker, live da Deribit, `null` dove l'exchange
non li espone), `history_start` (data inizio storico, live da Deribit), non li espone), `history_start` (data inizio storico, live da Deribit),
`type`, `tick_size` e un blocco `native` lossless. `exchange` opzionale → `type`, `tick_size` e un blocco `native` lossless. `exchange` opzionale →
filtra una sorgente; se omesso fan-out su tutti. filtra una sorgente; se omesso fan-out su tutti. Filtri avanzati Deribit
(`kind`, `expiry_*`, `strike_*`, `min_open_interest`) e paginazione
(`offset`/`limit`, con `meta.deribit.has_more`).
- **`get_historical`** — `{exchange?, instrument, interval, start_date, - **`get_historical`** — `{exchange?, instrument, interval, start_date,
end_date}`. Con `exchange` ritorna il **singolo** exchange; **omettendolo** end_date}`. Con `exchange` ritorna il **singolo** exchange; **omettendolo**
fa il **consenso** multi-exchange (mediana OHLC, `sources`, fa il **consenso** multi-exchange (mediana OHLC, `sources`,
+27 -18
View File
@@ -51,15 +51,16 @@ richiede `X-Bot-Tag`.
| Namespace | Tool | Tipo | Note | | Namespace | Tool | Tipo | Note |
|---|---|---|---| |---|---|---|---|
| `/mcp/tools/*` | 3 | **interfaccia comune** | `get_instruments` + `get_historical` + `get_indicators` (single exchange o consensus) | | `/mcp/tools/*` | 3 | **interfaccia comune** | `get_instruments` + `get_historical` + `get_indicators` (single exchange o consensus) |
| `/mcp-deribit/tools/*` | 32 | exchange (options-first) | DVOL, GEX, dealer gamma, spread payoff | | `/mcp-deribit/tools/*` | 31 | exchange (options-first) | DVOL, GEX, dealer gamma, spread payoff |
| `/mcp-hyperliquid/tools/*` | 14 | exchange (perp DEX) | L1 signing, leverage cap | | `/mcp-hyperliquid/tools/*` | 15 | exchange (perp DEX) | L1 signing, leverage cap |
| `/mcp-ibkr/tools/*` | 24 | exchange (multi-asset broker) | OAuth 1.0a, streaming WS, bracket/OCO/OTO | | `/mcp-ibkr/tools/*` | 24 | exchange (multi-asset broker) | OAuth 1.0a, streaming WS, bracket/OCO/OTO |
| `/mcp-macro/tools/*` | 11 | data provider (read-only) | yields, FRED, COT | | `/mcp-macro/tools/*` | 11 | data provider (read-only) | yields, FRED, COT |
| `/mcp-sentiment/tools/*` | 9 | data provider (read-only) | news, social, funding cross-exchange | | `/mcp-sentiment/tools/*` | 9 | data provider (read-only) | news, social, funding cross-exchange |
`get_historical` e `get_indicators` vivono **solo** sull'interfaccia comune `get_instruments`, `get_historical` e `get_indicators` vivono **solo**
`/mcp` (non più per-exchange). I restanti tool per-exchange (ticker, ordini, sull'interfaccia comune `/mcp` (non più per-exchange). I restanti tool
analytics options, …) restano sui rispettivi router `/mcp-{exchange}`. per-exchange (ticker, ordini, analytics options, …) restano sui rispettivi
router `/mcp-{exchange}`.
--- ---
@@ -76,10 +77,16 @@ Request body:
| Campo | Tipo | Default | Note | | Campo | Tipo | Default | Note |
|---|---|---|---| |---|---|---|---|
| `exchange` | `"deribit"\|"hyperliquid"` \| null | `null` | se omesso → fan-out su tutti gli exchange integrati | | `exchange` | `"deribit"\|"hyperliquid"` \| null | `null` | se omesso → fan-out su tutti gli exchange integrati |
| `currency` | str | `"BTC"` | solo Deribit | | `currency` | str | `"BTC"` | filtro Deribit |
| `kind` | str \| null | `"future"` | solo Deribit (`future`/`option`/`spot`) | | `kind` | str \| null | `"future"` | filtro Deribit (`future`/`option`/`spot`) |
| `expiry_from` / `expiry_to` | str \| null | `null` | filtro Deribit (data scadenza) |
| `strike_min` / `strike_max` | float \| null | `null` | filtro Deribit (opzioni) |
| `min_open_interest` | float \| null | `null` | filtro Deribit |
| `offset` / `limit` | int | `0` / `100` | paginazione Deribit |
Ogni elemento di `instruments[]` ha **schema uniforme**: I filtri/paginazione si applicano a **Deribit**; gli exchange che elencano
tutto (es. Hyperliquid `get_markets`) li ignorano. Ogni elemento di
`instruments[]` ha **schema uniforme**:
```json ```json
{ {
@@ -94,7 +101,9 @@ Ogni elemento di `instruments[]` ha **schema uniforme**:
} }
``` ```
Risposta: `{ "instruments": [...], "failed_sources": [{exchange, error}] }`. Risposta: `{ "instruments": [...], "failed_sources": [{exchange, error}],
"meta": { "deribit": {total, offset, limit, has_more} } }`. Il blocco `meta`
riporta la paginazione per-sorgente quando disponibile (oggi: Deribit).
> **Copertura `fees` / `history_start`** — popolati live dall'upstream dove > **Copertura `fees` / `history_start`** — popolati live dall'upstream dove
> disponibili. Oggi: **Deribit** li espone (commissioni maker/taker + > disponibili. Oggi: **Deribit** li espone (commissioni maker/taker +
@@ -152,13 +161,12 @@ sources_used }` (+ `failed_sources` in modalità consenso).
### Market data ### Market data
- `get_ticker`, `get_ticker_batch` - `get_ticker`, `get_ticker_batch`
- `get_instruments`
- `get_orderbook`, `get_orderbook_imbalance` - `get_orderbook`, `get_orderbook_imbalance`
- `get_trade_history` - `get_trade_history`
> Lo storico OHLCV e gli indicatori tecnici NON sono più qui: usa > Lista strumenti, storico OHLCV e indicatori tecnici NON sono più qui: usa
> `/mcp/tools/get_historical` e `/mcp/tools/get_indicators` (sezione 5) con > `/mcp/tools/get_instruments`, `/mcp/tools/get_historical` e
> `exchange="deribit"`. > `/mcp/tools/get_indicators` (sezione 5) con `exchange="deribit"`.
### Account ### Account
- `get_positions` - `get_positions`
@@ -333,15 +341,16 @@ uniforme `candles: [{timestamp(ms), open, high, low, close, volume}]`.
### Tool dati comuni (`/mcp`) ### Tool dati comuni (`/mcp`)
| Tool | Request body | Risposta (campi principali) | | Tool | Request body | Risposta (campi principali) |
|---|---|---| |---|---|---|
| `get_instruments` | `{exchange?, currency:"BTC", kind:"future"}` | `instruments[]` uniforme: `exchange`, `symbol`, `asset_class`, `type`, `fees`, `history_start`, `tick_size`, `native` · `failed_sources[]` | | `get_instruments` | `{exchange?, currency:"BTC", kind:"future", expiry_from?, expiry_to?, strike_min?, strike_max?, min_open_interest?, offset:0, limit:100}` | `instruments[]` uniforme: `exchange`, `symbol`, `asset_class`, `type`, `fees`, `history_start`, `tick_size`, `native` · `failed_sources[]` · `meta:{deribit:{total,offset,limit,has_more}}` |
| `get_historical` | `{exchange?, instrument, interval, start_date, end_date, asset_class?}` | single: `{exchange, instrument, interval, candles, sources_used}` · consensus (no `exchange`): `+ div_pct`/`sources` per bar, `failed_sources` | | `get_historical` | `{exchange?, instrument, interval, start_date, end_date, asset_class?}` | single: `{exchange, instrument, interval, candles, sources_used}` · consensus (no `exchange`): `+ div_pct`/`sources` per bar, `failed_sources` |
| `get_indicators` | come `get_historical` + `indicators:["rsi","atr",…]` | `{exchange, instrument, interval, indicators:{…}, candles_used, sources_used}` | | `get_indicators` | come `get_historical` + `indicators:["rsi","atr",…]` | `{exchange, instrument, interval, indicators:{…}, candles_used, sources_used}` |
`get_instruments` Deribit (sorgente grezza): `instruments[]` include `name`, Sorgenti normalizzate dentro `get_instruments`: Deribit espone `name`,
`expiry`, `option_type`, `tick_size`, `min_trade_amount`, `maker_commission`, `expiry`, `option_type`, `tick_size`, `min_trade_amount`, `maker_commission`,
`taker_commission`, `creation_timestamp`; Hyperliquid `get_markets` (sorgente): `taker_commission`, `creation_timestamp` (→ `fees`/`history_start`); Hyperliquid
`{asset, mark_price, funding_rate, open_interest, volume_24h, max_leverage}`. `get_markets` espone `{asset, mark_price, funding_rate, open_interest,
Ogni risposta `/tools/*` riceve inoltre `data_timestamp` dal middleware. volume_24h, max_leverage}`. Ogni risposta `/tools/*` riceve inoltre
`data_timestamp` dal middleware.
### Convenzione simboli Deribit ### Convenzione simboli Deribit
- **BTC/ETH** → perpetui *inverse*: `BTC-PERPETUAL`, `ETH-PERPETUAL` - **BTC/ETH** → perpetui *inverse*: `BTC-PERPETUAL`, `ETH-PERPETUAL`
+26 -9
View File
@@ -69,23 +69,29 @@ class UnifiedClient:
# ── instruments ────────────────────────────────────────────────── # ── instruments ──────────────────────────────────────────────────
async def _instruments_one( async def _instruments_one(
self, exchange: str, currency: str, kind: str | None, self, exchange: str, filters: dict[str, Any],
) -> tuple[str, list[dict[str, Any]] | Exception]: ) -> tuple[str, list[dict[str, Any]] | Exception, dict[str, Any] | None]:
try: try:
client = await self._registry.get(exchange, self._env) client = await self._registry.get(exchange, self._env)
if exchange == "deribit": if exchange == "deribit":
resp = await client.get_instruments(currency=currency, kind=kind) resp = await client.get_instruments(**filters)
return exchange, normalize_deribit(resp.get("instruments", [])) meta = {k: resp[k] for k in ("total", "offset", "limit", "has_more")
if k in resp}
return exchange, normalize_deribit(resp.get("instruments", [])), meta
if exchange == "hyperliquid": if exchange == "hyperliquid":
rows = await client.get_markets() rows = await client.get_markets()
return exchange, normalize_hyperliquid(rows) return exchange, normalize_hyperliquid(rows), None
raise ValueError(f"no instrument normalizer for {exchange}") raise ValueError(f"no instrument normalizer for {exchange}")
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
return exchange, e return exchange, e, None
async def get_instruments( async def get_instruments(
self, *, exchange: str | None = None, self, *, exchange: str | None = None,
currency: str = "BTC", kind: str | None = "future", currency: str = "BTC", kind: str | None = "future",
expiry_from: str | None = None, expiry_to: str | None = None,
strike_min: float | None = None, strike_max: float | None = None,
min_open_interest: float | None = None,
offset: int = 0, limit: int = 100,
) -> dict[str, Any]: ) -> dict[str, Any]:
if exchange is not None and exchange not in UNIFIED_EXCHANGES: if exchange is not None and exchange not in UNIFIED_EXCHANGES:
raise HTTPException( raise HTTPException(
@@ -94,18 +100,29 @@ class UnifiedClient:
f"supported: {list(UNIFIED_EXCHANGES)}", f"supported: {list(UNIFIED_EXCHANGES)}",
) )
targets = [exchange] if exchange else list(UNIFIED_EXCHANGES) targets = [exchange] if exchange else list(UNIFIED_EXCHANGES)
# Deribit-specific filters; venues that list everything ignore them.
deribit_filters = {
"currency": currency, "kind": kind,
"expiry_from": expiry_from, "expiry_to": expiry_to,
"strike_min": strike_min, "strike_max": strike_max,
"min_open_interest": min_open_interest,
"offset": offset, "limit": limit,
}
results = await asyncio.gather( results = await asyncio.gather(
*(self._instruments_one(ex, currency, kind) for ex in targets) *(self._instruments_one(ex, deribit_filters) for ex in targets)
) )
instruments: list[dict[str, Any]] = [] instruments: list[dict[str, Any]] = []
failed: list[dict[str, str]] = [] failed: list[dict[str, str]] = []
for ex, payload in results: meta: dict[str, Any] = {}
for ex, payload, ex_meta in results:
if isinstance(payload, Exception): if isinstance(payload, Exception):
failed.append({"exchange": ex, "error": f"{type(payload).__name__}: {payload}"}) failed.append({"exchange": ex, "error": f"{type(payload).__name__}: {payload}"})
else: else:
instruments.extend(payload) instruments.extend(payload)
if ex_meta:
meta[ex] = ex_meta
if not instruments and failed: if not instruments and failed:
raise HTTPException( raise HTTPException(
@@ -113,7 +130,7 @@ class UnifiedClient:
detail={"error": "all sources failed", "failed_sources": failed}, detail={"error": "all sources failed", "failed_sources": failed},
) )
return {"instruments": instruments, "failed_sources": failed} return {"instruments": instruments, "failed_sources": failed, "meta": meta}
# ── historical ─────────────────────────────────────────────────── # ── historical ───────────────────────────────────────────────────
+17 -2
View File
@@ -34,8 +34,16 @@ def _coerce_indicators(v):
class GetInstrumentsReq(BaseModel): class GetInstrumentsReq(BaseModel):
exchange: Exchange | None = None # None → fan-out over all integrated venues exchange: Exchange | None = None # None → fan-out over all integrated venues
currency: str = "BTC" # Deribit only # Deribit-only filters (ignored by venues that list everything, e.g. Hyperliquid)
kind: str | None = "future" # Deribit only: future | option | spot currency: str = "BTC"
kind: str | None = "future" # future | option | spot
expiry_from: str | None = None
expiry_to: str | None = None
strike_min: float | None = None
strike_max: float | None = None
min_open_interest: float | None = None
offset: int = 0
limit: int = 100
class GetHistoricalReq(BaseModel): class GetHistoricalReq(BaseModel):
@@ -69,6 +77,13 @@ async def get_instruments(client: UnifiedClient, params: GetInstrumentsReq) -> d
exchange=params.exchange, exchange=params.exchange,
currency=params.currency, currency=params.currency,
kind=params.kind, kind=params.kind,
expiry_from=params.expiry_from,
expiry_to=params.expiry_to,
strike_min=params.strike_min,
strike_max=params.strike_max,
min_open_interest=params.min_open_interest,
offset=params.offset,
limit=params.limit,
) )
@@ -51,16 +51,6 @@ class GetTickerBatchReq(BaseModel):
return self return self
class GetInstrumentsReq(BaseModel):
currency: str
kind: str | None = None
expiry_from: str | None = None
expiry_to: str | None = None
strike_min: float | None = None
strike_max: float | None = None
min_open_interest: float | None = None
limit: int = 100
offset: int = 0
class GetOrderbookReq(BaseModel): class GetOrderbookReq(BaseModel):
@@ -268,20 +258,6 @@ async def get_ticker_batch(client: DeribitClient, params: GetTickerBatchReq) ->
return await client.get_ticker_batch(params.instrument_names) return await client.get_ticker_batch(params.instrument_names)
async def get_instruments(client: DeribitClient, params: GetInstrumentsReq) -> dict:
return await client.get_instruments(
currency=params.currency,
kind=params.kind,
expiry_from=params.expiry_from,
expiry_to=params.expiry_to,
strike_min=params.strike_min,
strike_max=params.strike_max,
min_open_interest=params.min_open_interest,
limit=params.limit,
offset=params.offset,
)
async def get_orderbook(client: DeribitClient, params: GetOrderbookReq) -> dict: async def get_orderbook(client: DeribitClient, params: GetOrderbookReq) -> dict:
return await client.get_orderbook(params.instrument_name, params.depth) return await client.get_orderbook(params.instrument_name, params.depth)
-7
View File
@@ -73,13 +73,6 @@ def make_router() -> APIRouter:
): ):
return await t.get_ticker_batch(client, params) return await t.get_ticker_batch(client, params)
@r.post("/tools/get_instruments")
async def _get_instruments(
params: t.GetInstrumentsReq,
client: DeribitClient = Depends(get_deribit_client),
):
return await t.get_instruments(client, params)
@r.post("/tools/get_orderbook") @r.post("/tools/get_orderbook")
async def _get_orderbook( async def _get_orderbook(
params: t.GetOrderbookReq, params: t.GetOrderbookReq,
+2 -1
View File
@@ -30,7 +30,8 @@ def test_app_boots_and_health_responds(monkeypatch):
assert "/mcp/tools/get_instruments" in paths assert "/mcp/tools/get_instruments" in paths
assert "/mcp/tools/get_historical" in paths assert "/mcp/tools/get_historical" in paths
assert "/mcp/tools/get_indicators" in paths assert "/mcp/tools/get_indicators" in paths
# get_historical / get_indicators are common-only, not per-exchange # get_instruments / get_historical / get_indicators are common-only
assert "/mcp-deribit/tools/get_instruments" not in paths
assert "/mcp-deribit/tools/get_historical" not in paths assert "/mcp-deribit/tools/get_historical" not in paths
assert "/mcp-deribit/tools/get_technical_indicators" not in paths assert "/mcp-deribit/tools/get_technical_indicators" not in paths
assert "/mcp-hyperliquid/tools/get_historical" not in paths assert "/mcp-hyperliquid/tools/get_historical" not in paths
+9 -2
View File
@@ -38,14 +38,21 @@ post() {
echo " OK ${path} → HTTP ${status}" echo " OK ${path} → HTTP ${status}"
} }
echo "==> get_instruments exchange=deribit (fees + history_start live)" echo "==> get_instruments exchange=deribit (fees + history_start live + meta)"
post "/mcp/tools/get_instruments" '{"exchange":"deribit","currency":"BTC","kind":"future"}' 200 \ post "/mcp/tools/get_instruments" '{"exchange":"deribit","currency":"BTC","kind":"future"}' 200 \
"assert d['instruments']; i=d['instruments'][0]; assert i['exchange']=='deribit'; assert 'fees' in i and 'history_start' in i; print(' ', i['symbol'], i['fees'], i['history_start'])" "assert d['instruments']; i=d['instruments'][0]; assert i['exchange']=='deribit'; assert 'fees' in i and 'history_start' in i; assert 'has_more' in d['meta']['deribit']; print(' ', i['symbol'], i['fees'], i['history_start'], '| meta:', d['meta']['deribit'])"
echo "==> get_instruments deribit option chain paginato (kind=option, limit=5)"
post "/mcp/tools/get_instruments" '{"exchange":"deribit","currency":"BTC","kind":"option","limit":5,"offset":0}' 200 \
"assert len(d['instruments'])<=5; assert d['meta']['deribit']['limit']==5; print(' got:', len(d['instruments']), '| total:', d['meta']['deribit'].get('total'), '| has_more:', d['meta']['deribit'].get('has_more'))"
echo "==> get_instruments fan-out (deribit + hyperliquid)" echo "==> get_instruments fan-out (deribit + hyperliquid)"
post "/mcp/tools/get_instruments" '{}' 200 \ post "/mcp/tools/get_instruments" '{}' 200 \
"exs={i['exchange'] for i in d['instruments']}; assert {'deribit','hyperliquid'} <= exs, exs; print(' venues:', sorted(exs))" "exs={i['exchange'] for i in d['instruments']}; assert {'deribit','hyperliquid'} <= exs, exs; print(' venues:', sorted(exs))"
echo "==> get_instruments NON più su /mcp-deribit → 404"
post "/mcp-deribit/tools/get_instruments" '{"currency":"BTC"}' 404
echo "==> get_historical SINGLE deribit BTC-PERPETUAL 1h" echo "==> get_historical SINGLE deribit BTC-PERPETUAL 1h"
post "/mcp/tools/get_historical" "{\"exchange\":\"deribit\",\"instrument\":\"BTC-PERPETUAL\",\"interval\":\"1h\",\"start_date\":\"${start}\",\"end_date\":\"${end}\"}" 200 \ post "/mcp/tools/get_historical" "{\"exchange\":\"deribit\",\"instrument\":\"BTC-PERPETUAL\",\"interval\":\"1h\",\"start_date\":\"${start}\",\"end_date\":\"${end}\"}" 200 \
"assert d['exchange']=='deribit'; assert d['candles']; c=d['candles'][0]; assert {'timestamp','open','high','low','close','volume'} <= set(c); print(' candles:', len(d['candles']))" "assert d['exchange']=='deribit'; assert d['candles']; c=d['candles'][0]; assert {'timestamp','open','high','low','close','volume'} <= set(c); print(' candles:', len(d['candles']))"
+30 -2
View File
@@ -68,11 +68,18 @@ class _FakeDeribit:
self._raises = raises self._raises = raises
async def get_instruments(self, **kwargs: Any) -> dict[str, Any]: async def get_instruments(self, **kwargs: Any) -> dict[str, Any]:
return {"instruments": [{ self.inst_call = kwargs
return {
"instruments": [{
"name": "BTC-PERPETUAL", "kind": "future", "tick_size": 0.5, "name": "BTC-PERPETUAL", "kind": "future", "tick_size": 0.5,
"maker_commission": 0.0, "taker_commission": 0.0005, "maker_commission": 0.0, "taker_commission": 0.0005,
"creation_timestamp": 1534377600000, "creation_timestamp": 1534377600000,
}]} }],
"total": 1,
"offset": kwargs.get("offset", 0),
"limit": kwargs.get("limit", 100),
"has_more": False,
}
async def get_historical(self, **kwargs: Any) -> dict[str, Any]: async def get_historical(self, **kwargs: Any) -> dict[str, Any]:
if self._raises: if self._raises:
@@ -136,6 +143,27 @@ async def test_get_instruments_unsupported_exchange_raises_400():
assert exc.value.status_code == 400 assert exc.value.status_code == 400
@pytest.mark.asyncio
async def test_get_instruments_deribit_filters_and_meta():
fake = _FakeDeribit()
uc = UnifiedClient(_FakeRegistry({"deribit": fake}), env="mainnet")
out = await uc.get_instruments(
exchange="deribit", currency="ETH", kind="option",
strike_min=1000.0, expiry_to="2026-12-31", min_open_interest=5.0,
offset=5, limit=10,
)
# advanced Deribit filters reach the client
assert fake.inst_call["currency"] == "ETH"
assert fake.inst_call["kind"] == "option"
assert fake.inst_call["strike_min"] == 1000.0
assert fake.inst_call["expiry_to"] == "2026-12-31"
assert fake.inst_call["min_open_interest"] == 5.0
assert fake.inst_call["offset"] == 5 and fake.inst_call["limit"] == 10
# pagination metadata surfaced per source
assert out["meta"]["deribit"]["limit"] == 10
assert out["meta"]["deribit"]["has_more"] is False
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_instruments_partial_failure_reports_failed(): async def test_get_instruments_partial_failure_reports_failed():
uc = UnifiedClient(_FakeRegistry({"deribit": _FakeDeribit()}), env="mainnet") uc = UnifiedClient(_FakeRegistry({"deribit": _FakeDeribit()}), env="mainnet")