feat(V2): migrazione sentiment completa (read-only, env ignored)

- exchanges/sentiment/{client,fetchers,tools}.py: SentimentClient wrapper stateless (cryptopanic_key, lunarcrush_key)
- routers/sentiment.py: 9 tool POST sotto /mcp-sentiment (news, social, funding, OI, liquidations, cointegration)
- exchanges/__init__.py: branch builder per sentiment (env ignored)
- tests/unit/exchanges/sentiment: migrato test_fetchers, scartato test_server_acl V1-only
- tests/unit/test_exchanges_builder.py: aggiunto test_build_client_sentiment_no_env_distinction
- fetchers.py: env var lookup allineato a LUNARCRUSH_KEY (con fallback LUNARCRUSH_API_KEY)

241 test passano.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-30 18:46:48 +02:00
parent 88bd4e7bde
commit f56df197e1
9 changed files with 1253 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
"""Router /mcp-sentiment/* — read-only data provider.
Sentiment non distingue testnet/mainnet (CryptoPanic, LunarCrush e gli endpoint
pubblici di funding/OI multi-exchange sono unici), ma manteniamo la signature
`Environment` per uniformità con gli altri router. Tutti i tool sono READ —
niente write, niente leverage_cap.
"""
from __future__ import annotations
from typing import Literal
from fastapi import APIRouter, Depends, Request
from cerbero_mcp.client_registry import ClientRegistry
from cerbero_mcp.exchanges.sentiment import tools as t
from cerbero_mcp.exchanges.sentiment.client import SentimentClient
Environment = Literal["testnet", "mainnet"]
def get_environment(request: Request) -> Environment:
return request.state.environment
async def get_sentiment_client(
request: Request, env: Environment = Depends(get_environment)
) -> SentimentClient:
registry: ClientRegistry = request.app.state.registry
return await registry.get("sentiment", env)
def make_router() -> APIRouter:
r = APIRouter(prefix="/mcp-sentiment", tags=["sentiment"])
@r.post("/tools/get_crypto_news")
async def _get_crypto_news(
params: t.GetCryptoNewsReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_crypto_news(client, params)
@r.post("/tools/get_world_news")
async def _get_world_news(
params: t.GetWorldNewsReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_world_news(client, params)
@r.post("/tools/get_social_sentiment")
async def _get_social_sentiment(
params: t.GetSocialSentimentReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_social_sentiment(client, params)
@r.post("/tools/get_funding_rates")
async def _get_funding_rates(
params: t.GetFundingRatesReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_funding_rates(client, params)
@r.post("/tools/get_funding_arb_spread")
async def _get_funding_arb_spread(
params: t.GetFundingArbSpreadReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_funding_arb_spread(client, params)
@r.post("/tools/get_cross_exchange_funding")
async def _get_cross_exchange_funding(
params: t.GetCrossExchangeFundingReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_cross_exchange_funding(client, params)
@r.post("/tools/get_oi_history")
async def _get_oi_history(
params: t.GetOiHistoryReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_oi_history(client, params)
@r.post("/tools/get_liquidation_heatmap")
async def _get_liquidation_heatmap(
params: t.GetLiquidationHeatmapReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_liquidation_heatmap(client, params)
@r.post("/tools/get_cointegration_pairs")
async def _get_cointegration_pairs(
params: t.GetCointegrationPairsReq,
client: SentimentClient = Depends(get_sentiment_client),
):
return await t.get_cointegration_pairs(client, params)
return r