f4faef6fd1
Integra due nuovi filtri dal pacchetto quant indicators rilasciato in Cerbero_mcp (commit a13e3fe). 335 test pass, mypy strict pulito, ruff clean. Filtri (§2.8 — nuovo): - dealer-gamma: blocca entry quando total_net_dealer_gamma < dealer_gamma_min (default 0). Long-gamma regime favorisce credit spread (vol-suppressing dealer flow); short-gamma flow lo amplifica ed è da evitare. - liquidation-heatmap: blocca entry quando il segnale euristico di cerbero-sentiment riporta long o short squeeze risk = "high" (cluster di liquidations imminenti entro 24h). Entrambi sono best-effort: se il tool MCP fallisce o restituisce dati anomali l'entry_cycle popola EntryContext con None e validate_entry salta il gate per non bloccare entry su problemi infrastrutturali. Wrapper: - DeribitClient.dealer_gamma_profile_eth → DealerGammaSnapshot. - SentimentClient.liquidation_heatmap → LiquidationHeatmap con property has_high_squeeze_risk. Schema: - EntryConfig.dealer_gamma_min, dealer_gamma_filter_enabled, liquidation_filter_enabled. - EntryContext.dealer_net_gamma, liquidation_squeeze_risk_high opzionali. - strategy.yaml: nuovi campi documentati con commento + hash ricalcolato (4c2be4c5...). Documentazione: - docs/04-mcp-integration.md riscritto al modello attuale (HTTP REST, no mcp SDK, no memory/brain-bridge, place_combo_order documentato, environment_info al boot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
4.3 KiB
Python
148 lines
4.3 KiB
Python
"""Tests for SentimentClient."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
from pytest_httpx import HTTPXMock
|
|
|
|
from cerbero_bite.clients._base import HttpToolClient
|
|
from cerbero_bite.clients._exceptions import McpDataAnomalyError
|
|
from cerbero_bite.clients.sentiment import (
|
|
EXCHANGE_PERIODS_PER_YEAR,
|
|
SentimentClient,
|
|
)
|
|
|
|
|
|
def _client() -> SentimentClient:
|
|
http = HttpToolClient(
|
|
service="sentiment",
|
|
base_url="http://mcp-sentiment:9014",
|
|
token="t",
|
|
retry_max=1,
|
|
)
|
|
return SentimentClient(http)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_median_annualises_per_exchange_period(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
url="http://mcp-sentiment:9014/tools/get_cross_exchange_funding",
|
|
json={
|
|
"snapshot": {
|
|
"ETH": {
|
|
"binance": 0.0001,
|
|
"bybit": 0.0002,
|
|
"okx": 0.00015,
|
|
"hyperliquid": 0.00002,
|
|
}
|
|
}
|
|
},
|
|
)
|
|
median = await _client().funding_cross_median_annualized("eth")
|
|
# Annualised values (Decimal):
|
|
# binance: 0.0001 * 1095 = 0.1095
|
|
# bybit: 0.0002 * 1095 = 0.2190
|
|
# okx: 0.00015 * 1095 = 0.16425
|
|
# hyperliquid:0.00002 * 8760 = 0.17520
|
|
# sorted: 0.1095, 0.16425, 0.17520, 0.2190 → median = (0.16425+0.17520)/2 = 0.169725
|
|
assert median == Decimal("0.169725")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_median_skips_missing_venues(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
json={
|
|
"snapshot": {
|
|
"ETH": {
|
|
"binance": 0.0001,
|
|
"bybit": None,
|
|
"okx": None,
|
|
"hyperliquid": None,
|
|
}
|
|
}
|
|
}
|
|
)
|
|
median = await _client().funding_cross_median_annualized("ETH")
|
|
assert median == Decimal("0.0001") * Decimal("1095")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_anomaly_when_snapshot_missing(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(json={"snapshot": {}})
|
|
with pytest.raises(McpDataAnomalyError, match="snapshot missing"):
|
|
await _client().funding_cross_median_annualized("ETH")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_anomaly_when_no_venue_responded(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
json={
|
|
"snapshot": {
|
|
"ETH": {
|
|
"binance": None,
|
|
"bybit": None,
|
|
"okx": None,
|
|
"hyperliquid": None,
|
|
}
|
|
}
|
|
}
|
|
)
|
|
with pytest.raises(McpDataAnomalyError, match="no funding venues"):
|
|
await _client().funding_cross_median_annualized("ETH")
|
|
|
|
|
|
def test_periods_table_covers_documented_venues() -> None:
|
|
assert set(EXCHANGE_PERIODS_PER_YEAR) == {
|
|
"binance", "bybit", "okx", "hyperliquid"
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_liquidation_heatmap_parses_high_risk(httpx_mock: HTTPXMock) -> None:
|
|
httpx_mock.add_response(
|
|
url="http://mcp-sentiment:9014/tools/get_liquidation_heatmap",
|
|
json={
|
|
"asset": "ETH",
|
|
"avg_funding_rate": 0.00012,
|
|
"oi_delta_pct_4h": 6.5,
|
|
"oi_delta_pct_24h": 8.2,
|
|
"long_squeeze_risk": "high",
|
|
"short_squeeze_risk": "low",
|
|
},
|
|
)
|
|
out = await _client().liquidation_heatmap("eth")
|
|
assert out.asset == "ETH"
|
|
assert out.avg_funding_rate == Decimal("0.00012")
|
|
assert out.long_squeeze_risk == "high"
|
|
assert out.has_high_squeeze_risk is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_liquidation_heatmap_unknown_risk_levels_default_to_low(
|
|
httpx_mock: HTTPXMock,
|
|
) -> None:
|
|
httpx_mock.add_response(
|
|
json={
|
|
"asset": "ETH",
|
|
"long_squeeze_risk": "extreme",
|
|
"short_squeeze_risk": None,
|
|
}
|
|
)
|
|
out = await _client().liquidation_heatmap("ETH")
|
|
assert out.long_squeeze_risk == "low"
|
|
assert out.short_squeeze_risk == "low"
|
|
assert out.has_high_squeeze_risk is False
|
|
|
|
|
|
def test_sentiment_client_rejects_wrong_service() -> None:
|
|
bad = HttpToolClient(
|
|
service="macro",
|
|
base_url="http://x:1",
|
|
token="t",
|
|
retry_max=1,
|
|
)
|
|
with pytest.raises(ValueError, match="requires service 'sentiment'"):
|
|
SentimentClient(bad)
|