feat: import 6 MCP services + common workspace

This commit is contained in:
AdrianoDev
2026-04-27 17:34:14 +02:00
parent 9676f22a8e
commit 6fc3d1d94f
67 changed files with 10693 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
from __future__ import annotations
import re
import pytest
from mcp_deribit.client import DeribitClient
from pytest_httpx import HTTPXMock
@pytest.fixture
def client():
return DeribitClient(client_id="cid", client_secret="csec", testnet=True)
AUTH_RESP = {"result": {"access_token": "tok", "expires_in": 3600}}
@pytest.mark.asyncio
async def test_get_ticker(httpx_mock: HTTPXMock, client: DeribitClient):
# public endpoint — no auth needed
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/ticker"),
json={
"result": {
"mark_price": 50000,
"last_price": 49900,
"best_bid_price": 49950,
"best_ask_price": 50050,
"instrument_name": "BTC-PERPETUAL",
"stats": {"volume": 1234.5},
"open_interest": 9999,
"greeks": None,
"mark_iv": None,
}
},
)
result = await client.get_ticker("BTC-PERPETUAL")
assert result["mark_price"] == 50000
assert result["bid"] == 49950
assert result["ask"] == 50050
# CER-003: perpetual returns conceptual greeks, not None
assert result["greeks"] == {"delta": 1.0, "gamma": 0.0, "vega": 0.0, "theta": 0.0, "rho": 0.0}
# CER-007: testnet flag present
assert result["testnet"] is True
@pytest.mark.asyncio
async def test_get_ticker_option_preserves_greeks(httpx_mock: HTTPXMock, client: DeribitClient):
real_greeks = {"delta": 0.42, "gamma": 0.001, "vega": 0.05, "theta": -0.02, "rho": 0.003}
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/ticker"),
json={
"result": {
"mark_price": 2500,
"last_price": 2500,
"best_bid_price": 2490,
"best_ask_price": 2510,
"instrument_name": "BTC-30APR26-75000-C",
"stats": {"volume": 5.0},
"open_interest": 100,
"greeks": real_greeks,
"mark_iv": 62.5,
}
},
)
result = await client.get_ticker("BTC-30APR26-75000-C")
assert result["greeks"] == real_greeks
assert result["mark_iv"] == 62.5
def test_is_testnet(client: DeribitClient):
info = client.is_testnet()
assert info["testnet"] is True
assert "test.deribit.com" in info["base_url"]
@pytest.mark.asyncio
async def test_get_instruments_pagination_and_filter(httpx_mock: HTTPXMock, client: DeribitClient):
items = []
for i, exp_ms in enumerate([1700000000000, 1776000000000, 1800000000000]):
items.append({
"instrument_name": f"BTC-inst-{i}",
"strike": 50000 + i * 10000,
"expiration_timestamp": exp_ms,
"option_type": "call",
"tick_size": 0.5,
"min_trade_amount": 0.1,
# CER-008: public/get_instruments non include OI in produzione
})
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_instruments"),
json={"result": items},
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_book_summary_by_currency"),
json={"result": [
{"instrument_name": "BTC-inst-0", "open_interest": 100.0},
{"instrument_name": "BTC-inst-1", "open_interest": 200.0},
{"instrument_name": "BTC-inst-2", "open_interest": 300.0},
]},
)
result = await client.get_instruments(
"BTC", kind="option", strike_min=55000, limit=1, offset=0
)
assert result["total"] == 2
assert len(result["instruments"]) == 1
assert result["has_more"] is True
assert result["testnet"] is True
assert result["instruments"][0]["strike"] >= 55000
# CER-008: OI merge da book_summary
assert result["instruments"][0]["open_interest"] in (200.0, 300.0)
@pytest.mark.asyncio
async def test_get_instruments_min_oi_filter(httpx_mock: HTTPXMock, client: DeribitClient):
"""CER-008: min_open_interest filtra server-side usando book_summary."""
items = [
{"instrument_name": "BTC-low-OI", "strike": 60000, "expiration_timestamp": 1800000000000,
"option_type": "call", "tick_size": 0.5, "min_trade_amount": 0.1},
{"instrument_name": "BTC-high-OI", "strike": 60000, "expiration_timestamp": 1800000000000,
"option_type": "call", "tick_size": 0.5, "min_trade_amount": 0.1},
]
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_instruments"),
json={"result": items},
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_book_summary_by_currency"),
json={"result": [
{"instrument_name": "BTC-low-OI", "open_interest": 5.0},
{"instrument_name": "BTC-high-OI", "open_interest": 500.0},
]},
)
result = await client.get_instruments("BTC", kind="option", min_open_interest=100)
assert result["total"] == 1
assert result["instruments"][0]["name"] == "BTC-high-OI"
assert result["instruments"][0]["open_interest"] == 500.0
@pytest.mark.asyncio
async def test_get_account_summary(httpx_mock: HTTPXMock, client: DeribitClient):
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"),
json=AUTH_RESP,
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/private/get_account_summary"),
json={"result": {"equity": 1000.0, "balance": 900.0, "currency": "USDC",
"margin_balance": 800.0, "available_funds": 700.0,
"unrealized_pnl": 50.0, "total_pnl": 100.0}},
)
result = await client.get_account_summary("USDC")
assert result["equity"] == 1000.0
assert result["balance"] == 900.0
@pytest.mark.asyncio
async def test_place_order(httpx_mock: HTTPXMock, client: DeribitClient):
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"),
json=AUTH_RESP,
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/private/buy"),
json={"result": {"order": {"order_id": "abc", "amount": 10, "order_state": "open"}, "trades": []}},
)
result = await client.place_order(
instrument_name="BTC-PERPETUAL",
side="buy",
amount=10,
type="limit",
price=50000,
)
assert result["order"]["order_id"] == "abc"
@pytest.mark.asyncio
async def test_get_positions(httpx_mock: HTTPXMock, client: DeribitClient):
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"),
json=AUTH_RESP,
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/private/get_positions"),
json={"result": [
{
"instrument_name": "BTC-PERPETUAL",
"size": 100.0,
"average_price": 48000.0,
"mark_price": 50000.0,
"floating_profit_loss": 200.0,
"realized_profit_loss": 50.0,
"leverage": 10,
}
]},
)
result = await client.get_positions("USDC")
assert len(result) == 1
assert result[0]["instrument"] == "BTC-PERPETUAL"
assert result[0]["direction"] == "long"
@pytest.mark.asyncio
async def test_get_dvol(httpx_mock: HTTPXMock, client: DeribitClient):
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/get_volatility_index_data"),
json={
"result": {
"data": [
[1700000000000, 55.0, 58.0, 54.0, 57.0],
[1700086400000, 57.0, 60.0, 56.0, 59.5],
],
"continuation": None,
}
},
)
result = await client.get_dvol("btc", "2024-01-01", "2024-01-02", "1D")
assert result["currency"] == "BTC"
assert result["latest"] == 59.5
assert len(result["candles"]) == 2
assert result["candles"][0]["close"] == 57.0
@pytest.mark.asyncio
async def test_cancel_order(httpx_mock: HTTPXMock, client: DeribitClient):
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"),
json=AUTH_RESP,
)
httpx_mock.add_response(
url=re.compile(r"https://test\.deribit\.com/api/v2/private/cancel"),
json={"result": {"order_id": "abc123", "order_state": "cancelled"}},
)
result = await client.cancel_order("abc123")
assert result["order_id"] == "abc123"
assert result["state"] == "cancelled"
@@ -0,0 +1,188 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from mcp_deribit.server import create_app
from option_mcp_common.auth import Principal, TokenStore
@pytest.fixture
def mock_client():
c = MagicMock()
c.get_ticker = AsyncMock(return_value={"mark_price": 50000})
c.get_instruments = AsyncMock(return_value=[])
c.get_orderbook = AsyncMock(return_value={"bids": [], "asks": []})
c.get_positions = AsyncMock(return_value=[])
c.get_account_summary = AsyncMock(return_value={"equity": 1000})
c.get_trade_history = AsyncMock(return_value=[])
c.get_historical = AsyncMock(return_value={"candles": []})
c.get_technical_indicators = AsyncMock(return_value={"rsi": 55.0})
c.place_order = AsyncMock(return_value={"order_id": "x"})
c.cancel_order = AsyncMock(return_value={"order_id": "x", "state": "cancelled"})
c.set_stop_loss = AsyncMock(return_value={"order_id": "x", "stop_price": 45000})
c.set_take_profit = AsyncMock(return_value={"order_id": "x", "tp_price": 55000})
c.close_position = AsyncMock(return_value={"closed": True})
c.set_leverage = AsyncMock(return_value={"state": "ok"})
return c
@pytest.fixture
def http(mock_client):
store = TokenStore(tokens={
"ct": Principal("core", {"core"}),
"ot": Principal("observer", {"observer"}),
})
app = create_app(client=mock_client, token_store=store)
return TestClient(app)
def test_health(http):
assert http.get("/health").status_code == 200
def test_get_ticker_core_ok(http):
r = http.post(
"/tools/get_ticker",
headers={"Authorization": "Bearer ct"},
json={"instrument_name": "BTC-PERPETUAL"},
)
assert r.status_code == 200
assert r.json()["mark_price"] == 50000
def test_get_ticker_observer_ok(http):
r = http.post(
"/tools/get_ticker",
headers={"Authorization": "Bearer ot"},
json={"instrument_name": "BTC-PERPETUAL"},
)
assert r.status_code == 200
def test_get_ticker_no_auth_401(http):
r = http.post("/tools/get_ticker", json={"instrument_name": "BTC-PERPETUAL"})
assert r.status_code == 401
def test_get_ticker_alias_instrument_ok(http, mock_client):
r = http.post(
"/tools/get_ticker",
headers={"Authorization": "Bearer ct"},
json={"instrument": "ETH"},
)
assert r.status_code == 200
mock_client.get_ticker.assert_awaited_with("ETH")
def test_place_order_core_ok(http):
r = http.post(
"/tools/place_order",
headers={"Authorization": "Bearer ct"},
json={"instrument_name": "BTC-PERPETUAL", "side": "buy", "amount": 10},
)
assert r.status_code == 200
def test_place_order_observer_forbidden(http):
r = http.post(
"/tools/place_order",
headers={"Authorization": "Bearer ot"},
json={"instrument_name": "BTC-PERPETUAL", "side": "buy", "amount": 10},
)
assert r.status_code == 403
def test_place_order_notional_cap_enforced(http):
"""CER-016: reject se notional > CERBERO_MAX_NOTIONAL (default 200)."""
r = http.post(
"/tools/place_order",
headers={"Authorization": "Bearer ct"},
json={
"instrument_name": "ETH-PERPETUAL",
"side": "buy",
"amount": 335, # USD — cap 200
},
)
assert r.status_code == 403
body = r.json()
assert body["error"]["code"] == "HARD_PROHIBITION"
def test_place_order_leverage_cap_enforced(http):
"""CER-016: reject leverage > 3x."""
r = http.post(
"/tools/place_order",
headers={"Authorization": "Bearer ct"},
json={
"instrument_name": "BTC-PERPETUAL",
"side": "buy",
"amount": 50,
"leverage": 50,
},
)
assert r.status_code == 403
body = r.json()
assert body["error"]["code"] == "HARD_PROHIBITION"
def test_place_order_reduce_only_skips_cap(http):
"""CER-016: reduce_only orders bypassano cap notional (è close)."""
r = http.post(
"/tools/place_order",
headers={"Authorization": "Bearer ct"},
json={
"instrument_name": "ETH-PERPETUAL",
"side": "sell",
"amount": 10000,
"reduce_only": True,
},
)
assert r.status_code == 200
def test_close_position_core_ok(http):
r = http.post(
"/tools/close_position",
headers={"Authorization": "Bearer ct"},
json={"instrument_name": "BTC-PERPETUAL"},
)
assert r.status_code == 200
def test_close_position_observer_forbidden(http):
r = http.post(
"/tools/close_position",
headers={"Authorization": "Bearer ot"},
json={"instrument_name": "BTC-PERPETUAL"},
)
assert r.status_code == 403
def test_cancel_order_observer_forbidden(http):
r = http.post(
"/tools/cancel_order",
headers={"Authorization": "Bearer ot"},
json={"order_id": "abc123"},
)
assert r.status_code == 403
def test_set_stop_loss_observer_forbidden(http):
r = http.post(
"/tools/set_stop_loss",
headers={"Authorization": "Bearer ot"},
json={"order_id": "abc123", "stop_price": 45000.0},
)
assert r.status_code == 403
def test_get_account_summary_observer_ok(http):
r = http.post(
"/tools/get_account_summary",
headers={"Authorization": "Bearer ot"},
json={"currency": "USDC"},
)
assert r.status_code == 200
assert r.json()["equity"] == 1000