refactor(mcp-deribit): replace risk_guard with local leverage_cap
This commit is contained in:
@@ -36,7 +36,7 @@ def main():
|
|||||||
core_token_file=os.environ.get("CORE_TOKEN_FILE"),
|
core_token_file=os.environ.get("CORE_TOKEN_FILE"),
|
||||||
observer_token_file=os.environ.get("OBSERVER_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(
|
uvicorn.run(
|
||||||
app,
|
app,
|
||||||
log_config=None, # CER-P5-009: delega al root JSON logger
|
log_config=None, # CER-P5-009: delega al root JSON logger
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import os
|
|||||||
from fastapi import Depends, FastAPI, HTTPException
|
from fastapi import Depends, FastAPI, HTTPException
|
||||||
from mcp_common.auth import Principal, TokenStore, require_principal
|
from mcp_common.auth import Principal, TokenStore, require_principal
|
||||||
from mcp_common.mcp_bridge import mount_mcp_endpoint
|
from mcp_common.mcp_bridge import mount_mcp_endpoint
|
||||||
from mcp_common.risk_guard import (
|
from mcp_deribit.leverage_cap import enforce_leverage as _enforce_leverage
|
||||||
enforce_aggregate,
|
from mcp_deribit.leverage_cap import get_max_leverage
|
||||||
enforce_leverage,
|
|
||||||
enforce_single_notional,
|
|
||||||
)
|
|
||||||
from mcp_common.server import build_app
|
from mcp_common.server import build_app
|
||||||
from pydantic import BaseModel, field_validator, model_validator
|
from pydantic import BaseModel, field_validator, model_validator
|
||||||
|
|
||||||
@@ -208,48 +205,6 @@ class ClosePositionReq(BaseModel):
|
|||||||
instrument_name: str
|
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 ---
|
# --- ACL helper ---
|
||||||
|
|
||||||
def _check(principal: Principal, *, core: bool = False, observer: bool = False) -> None:
|
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 ---
|
# --- 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
|
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
|
@asynccontextmanager
|
||||||
async def _lifespan(_app: FastAPI):
|
async def _lifespan(_app: FastAPI):
|
||||||
cap = enforce_leverage(None)
|
|
||||||
for inst in ("BTC-PERPETUAL", "ETH-PERPETUAL"):
|
for inst in ("BTC-PERPETUAL", "ETH-PERPETUAL"):
|
||||||
try:
|
try:
|
||||||
await client.set_leverage(inst, cap)
|
await client.set_leverage(inst, cap_default)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
yield
|
yield
|
||||||
@@ -476,15 +432,8 @@ def create_app(*, client: DeribitClient, token_store: TokenStore) -> FastAPI:
|
|||||||
body: PlaceOrderReq, principal: Principal = Depends(require_principal)
|
body: PlaceOrderReq, principal: Principal = Depends(require_principal)
|
||||||
):
|
):
|
||||||
_check(principal, core=True)
|
_check(principal, core=True)
|
||||||
lev = enforce_leverage(body.leverage)
|
lev = _enforce_leverage(body.leverage, creds=creds, exchange="deribit")
|
||||||
if not body.reduce_only:
|
if lev != cap_default:
|
||||||
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):
|
|
||||||
try:
|
try:
|
||||||
await client.set_leverage(body.instrument_name, lev)
|
await client.set_leverage(body.instrument_name, lev)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def http(mock_client):
|
|||||||
"ct": Principal("core", {"core"}),
|
"ct": Principal("core", {"core"}),
|
||||||
"ot": Principal("observer", {"observer"}),
|
"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)
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,24 +94,8 @@ def test_place_order_observer_forbidden(http):
|
|||||||
assert r.status_code == 403
|
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):
|
def test_place_order_leverage_cap_enforced(http):
|
||||||
"""CER-016: reject leverage > 3x."""
|
"""Reject leverage > max_leverage (da secret, default 3)."""
|
||||||
r = http.post(
|
r = http.post(
|
||||||
"/tools/place_order",
|
"/tools/place_order",
|
||||||
headers={"Authorization": "Bearer ct"},
|
headers={"Authorization": "Bearer ct"},
|
||||||
@@ -124,22 +108,12 @@ def test_place_order_leverage_cap_enforced(http):
|
|||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["error"]["code"] == "HARD_PROHIBITION"
|
err = body["error"]
|
||||||
|
assert err["code"] == "LEVERAGE_CAP_EXCEEDED"
|
||||||
|
details = err["details"]
|
||||||
def test_place_order_reduce_only_skips_cap(http):
|
assert details["exchange"] == "deribit"
|
||||||
"""CER-016: reduce_only orders bypassano cap notional (è close)."""
|
assert details["requested"] == 50
|
||||||
r = http.post(
|
assert details["max"] == 3
|
||||||
"/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):
|
def test_close_position_core_ok(http):
|
||||||
|
|||||||
Reference in New Issue
Block a user