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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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`
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']))"
|
||||||
|
|||||||
@@ -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
|
||||||
"name": "BTC-PERPETUAL", "kind": "future", "tick_size": 0.5,
|
return {
|
||||||
"maker_commission": 0.0, "taker_commission": 0.0005,
|
"instruments": [{
|
||||||
"creation_timestamp": 1534377600000,
|
"name": "BTC-PERPETUAL", "kind": "future", "tick_size": 0.5,
|
||||||
}]}
|
"maker_commission": 0.0, "taker_commission": 0.0005,
|
||||||
|
"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")
|
||||||
|
|||||||
Reference in New Issue
Block a user