chore: ruff py313, conftest unification, audit log, app factory comune

- pyproject.toml: ruff target-version py311 → py313 (auto-fix 42 lint
  warnings via UP rules); aggiunto consider_namespace_packages = true
  che risolve la collisione conftest tra servizi e permette di lanciare
  pytest sull'intera suite cross-servizio.

- mcp_common.audit: nuovo helper audit_write_op() con logger dedicato
  mcp.audit. Wirato su tutti i write endpoint di deribit, bybit, alpaca
  e hyperliquid (place_order, place_combo_order, cancel_*, set_*,
  close_*, transfer_*, switch_*, amend_*) con principal + target +
  payload non-sensibile + result summarizzato.

- mcp_common.app_factory: ExchangeAppSpec + run_exchange_main()
  centralizza il boilerplate dei __main__.py (configure_root_logging,
  fail_fast_if_missing, summarize, load creds, resolve_environment,
  load token store, uvicorn). I 4 __main__.py exchange ridotti da ~60
  LOC ognuno a ~25 LOC dichiarativi. mcp_common.env_validation
  promosso da mcp_deribit (mantenuto re-export shim per back-compat
  test_env_validation).

- 8 test nuovi (4 audit + 4 app_factory). Suite full: 450/450 verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-28 00:27:02 +02:00
parent a13e3fe045
commit 4d9db750be
45 changed files with 756 additions and 333 deletions
@@ -3,15 +3,16 @@ from __future__ import annotations
import os
from fastapi import Depends, FastAPI, HTTPException
from mcp_common.audit import audit_write_op
from mcp_common.auth import Principal, TokenStore, require_principal
from mcp_common.mcp_bridge import mount_mcp_endpoint
from mcp_common.environment import EnvironmentInfo
from mcp_hyperliquid.leverage_cap import enforce_leverage as _enforce_leverage
from mcp_hyperliquid.leverage_cap import get_max_leverage
from mcp_common.mcp_bridge import mount_mcp_endpoint
from mcp_common.server import build_app
from pydantic import BaseModel, field_validator, model_validator
from mcp_hyperliquid.client import HyperliquidClient
from mcp_hyperliquid.leverage_cap import enforce_leverage as _enforce_leverage
from mcp_hyperliquid.leverage_cap import get_max_leverage
# --- Body models ---
@@ -305,7 +306,7 @@ def create_app(
):
_check(principal, core=True)
_enforce_leverage(body.leverage, creds=creds, exchange="hyperliquid")
return await client.place_order(
result = await client.place_order(
instrument=body.instrument,
side=body.side,
amount=body.amount,
@@ -313,34 +314,67 @@ def create_app(
price=body.price,
reduce_only=body.reduce_only,
)
audit_write_op(
principal=principal, action="place_order", exchange="hyperliquid",
target=body.instrument,
payload={"side": body.side, "amount": body.amount, "type": body.type,
"price": body.price, "reduce_only": body.reduce_only,
"leverage": body.leverage},
result=result,
)
return result
@app.post("/tools/cancel_order", tags=["writes"])
async def t_cancel_order(
body: CancelOrderReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True)
return await client.cancel_order(body.order_id, body.instrument)
result = await client.cancel_order(body.order_id, body.instrument)
audit_write_op(
principal=principal, action="cancel_order", exchange="hyperliquid",
target=body.order_id, payload={"instrument": body.instrument}, result=result,
)
return result
@app.post("/tools/set_stop_loss", tags=["writes"])
async def t_set_sl(
body: SetStopLossReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True)
return await client.set_stop_loss(body.instrument, body.stop_price, body.size)
result = await client.set_stop_loss(body.instrument, body.stop_price, body.size)
audit_write_op(
principal=principal, action="set_stop_loss", exchange="hyperliquid",
target=body.instrument,
payload={"stop_price": body.stop_price, "size": body.size},
result=result,
)
return result
@app.post("/tools/set_take_profit", tags=["writes"])
async def t_set_tp(
body: SetTakeProfitReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True)
return await client.set_take_profit(body.instrument, body.tp_price, body.size)
result = await client.set_take_profit(body.instrument, body.tp_price, body.size)
audit_write_op(
principal=principal, action="set_take_profit", exchange="hyperliquid",
target=body.instrument,
payload={"tp_price": body.tp_price, "size": body.size},
result=result,
)
return result
@app.post("/tools/close_position", tags=["writes"])
async def t_close_position(
body: ClosePositionReq, principal: Principal = Depends(require_principal)
):
_check(principal, core=True)
return await client.close_position(body.instrument)
result = await client.close_position(body.instrument)
audit_write_op(
principal=principal, action="close_position", exchange="hyperliquid",
target=body.instrument, payload={}, result=result,
)
return result
# ───── MCP endpoint (/mcp) — bridge verso /tools/* ─────
port = int(os.environ.get("PORT", "9012"))