from __future__ import annotations import re import pytest from cerbero_mcp.exchanges.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_private_call_with_bad_auth_returns_error_envelope( httpx_mock: HTTPXMock, client: DeribitClient ): """Auth fallita (creds errate / scope mancante) → error envelope, non KeyError.""" httpx_mock.add_response( url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"), json={"error": {"code": 13004, "message": "invalid_credentials"}}, is_reusable=True, ) summary = await client.get_account_summary("USDC") assert summary["equity"] is None assert summary["balance"] is None assert "invalid_credentials" in summary["error"] positions = await client.get_positions("USDC") assert positions == [] @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_place_combo_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/create_combo"), json={ "result": { "id": "BTC-COMBO-1", "instrument_name": "BTC-COMBO-1", "state": "active", } }, ) httpx_mock.add_response( url=re.compile(r"https://test\.deribit\.com/api/v2/private/buy"), json={ "result": { "order": {"order_id": "ord-1", "order_state": "open"}, "trades": [], } }, ) legs = [ {"instrument_name": "BTC-30APR26-75000-C", "direction": "buy", "ratio": 1}, {"instrument_name": "BTC-30APR26-80000-C", "direction": "sell", "ratio": 1}, ] result = await client.place_combo_order( legs=legs, side="buy", amount=1, type="limit", price=0.05, label="vert-spread", ) assert result["combo_instrument"] == "BTC-COMBO-1" assert result["order"]["order_id"] == "ord-1" @pytest.mark.asyncio async def test_place_combo_order_create_combo_error(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/create_combo"), json={"error": {"code": -32602, "message": "Invalid leg"}}, ) result = await client.place_combo_order( legs=[{"instrument_name": "BTC-X", "direction": "buy", "ratio": 1}], side="buy", amount=1, type="market", ) assert result["state"] == "error" assert "Invalid leg" in str(result.get("error")) @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"