From ecb2d0e4c2230669305f8824f925b61885460135 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 27 Apr 2026 17:49:35 +0200 Subject: [PATCH] refactor(mcp-deribit): replace risk_guard with local leverage_cap --- .../mcp-deribit/src/mcp_deribit/__main__.py | 2 +- .../mcp-deribit/src/mcp_deribit/server.py | 69 +++---------------- services/mcp-deribit/tests/test_server_acl.py | 42 +++-------- 3 files changed, 18 insertions(+), 95 deletions(-) diff --git a/services/mcp-deribit/src/mcp_deribit/__main__.py b/services/mcp-deribit/src/mcp_deribit/__main__.py index a1450a2..811303b 100644 --- a/services/mcp-deribit/src/mcp_deribit/__main__.py +++ b/services/mcp-deribit/src/mcp_deribit/__main__.py @@ -36,7 +36,7 @@ def main(): core_token_file=os.environ.get("CORE_TOKEN_FILE"), observer_token_file=os.environ.get("OBSERVER_TOKEN_FILE"), ) - app = create_app(client=client, token_store=token_store) + app = create_app(client=client, token_store=token_store, creds=creds) uvicorn.run( app, log_config=None, # CER-P5-009: delega al root JSON logger diff --git a/services/mcp-deribit/src/mcp_deribit/server.py b/services/mcp-deribit/src/mcp_deribit/server.py index 99efb74..f1eb55e 100644 --- a/services/mcp-deribit/src/mcp_deribit/server.py +++ b/services/mcp-deribit/src/mcp_deribit/server.py @@ -5,11 +5,8 @@ import os from fastapi import Depends, FastAPI, HTTPException from mcp_common.auth import Principal, TokenStore, require_principal from mcp_common.mcp_bridge import mount_mcp_endpoint -from mcp_common.risk_guard import ( - enforce_aggregate, - enforce_leverage, - enforce_single_notional, -) +from mcp_deribit.leverage_cap import enforce_leverage as _enforce_leverage +from mcp_deribit.leverage_cap import get_max_leverage from mcp_common.server import build_app from pydantic import BaseModel, field_validator, model_validator @@ -208,48 +205,6 @@ class ClosePositionReq(BaseModel): instrument_name: str -# --- CER-016 notional helpers --- - -async def _compute_notional_deribit(client: DeribitClient, body: PlaceOrderReq) -> float: - """Stima notional in USD per un ordine Deribit. - - - Perp USDC: contract size = 1 USD → amount è già notional USD. - - Options: amount è in base asset (BTC/ETH) → moltiplica per index price. - - Altri perp BTC/ETH: amount in USD notional. - """ - name = body.instrument_name.upper() - if name.endswith("-PERPETUAL"): - return float(body.amount) - ref_price: float | None = body.price - if ref_price is None: - try: - tk = await client.get_ticker(body.instrument_name) - ref_price = tk.get("mark_price") or tk.get("last_price") - except Exception: - ref_price = None - if not ref_price: - return float(body.amount) - return float(body.amount) * float(ref_price) - - -async def _current_aggregate_deribit(client: DeribitClient) -> float: - """Somma notional posizioni aperte su Deribit (USDC).""" - try: - positions = await client.get_positions("USDC") - except Exception: - return 0.0 - total = 0.0 - for p in positions or []: - size = abs(float(p.get("size") or 0)) - name = str(p.get("instrument") or "").upper() - if name.endswith("-PERPETUAL"): - total += size - else: - mark = float(p.get("mark_price") or 0) - total += size * mark - return total - - # --- ACL helper --- def _check(principal: Principal, *, core: bool = False, observer: bool = False) -> None: @@ -264,16 +219,17 @@ def _check(principal: Principal, *, core: bool = False, observer: bool = False) # --- App factory --- -def create_app(*, client: DeribitClient, token_store: TokenStore) -> FastAPI: +def create_app(*, client: DeribitClient, token_store: TokenStore, creds: dict) -> FastAPI: from contextlib import asynccontextmanager - # CER-016: pre-set leverage 3x su perp principali al boot (best-effort). + cap_default = get_max_leverage(creds) + + # CER-016: pre-set leverage cap su perp principali al boot (best-effort). @asynccontextmanager async def _lifespan(_app: FastAPI): - cap = enforce_leverage(None) for inst in ("BTC-PERPETUAL", "ETH-PERPETUAL"): try: - await client.set_leverage(inst, cap) + await client.set_leverage(inst, cap_default) except Exception: pass yield @@ -476,15 +432,8 @@ def create_app(*, client: DeribitClient, token_store: TokenStore) -> FastAPI: body: PlaceOrderReq, principal: Principal = Depends(require_principal) ): _check(principal, core=True) - lev = enforce_leverage(body.leverage) - if not body.reduce_only: - notional = await _compute_notional_deribit(client, body) - enforce_single_notional( - notional, exchange="deribit", instrument=body.instrument_name - ) - agg = await _current_aggregate_deribit(client) - enforce_aggregate(agg, notional) - if lev != enforce_leverage(None): + lev = _enforce_leverage(body.leverage, creds=creds, exchange="deribit") + if lev != cap_default: try: await client.set_leverage(body.instrument_name, lev) except Exception: diff --git a/services/mcp-deribit/tests/test_server_acl.py b/services/mcp-deribit/tests/test_server_acl.py index cc667d5..f1059f1 100644 --- a/services/mcp-deribit/tests/test_server_acl.py +++ b/services/mcp-deribit/tests/test_server_acl.py @@ -34,7 +34,7 @@ def http(mock_client): "ct": Principal("core", {"core"}), "ot": Principal("observer", {"observer"}), }) - app = create_app(client=mock_client, token_store=store) + app = create_app(client=mock_client, token_store=store, creds={"max_leverage": 3}) return TestClient(app) @@ -94,24 +94,8 @@ def test_place_order_observer_forbidden(http): 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.""" + """Reject leverage > max_leverage (da secret, default 3).""" r = http.post( "/tools/place_order", headers={"Authorization": "Bearer ct"}, @@ -124,22 +108,12 @@ def test_place_order_leverage_cap_enforced(http): ) 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 + err = body["error"] + assert err["code"] == "LEVERAGE_CAP_EXCEEDED" + details = err["details"] + assert details["exchange"] == "deribit" + assert details["requested"] == 50 + assert details["max"] == 3 def test_close_position_core_ok(http):