Phase 4 hardening: dealer-gamma + liquidation-heatmap entry filters

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>
This commit is contained in:
2026-04-28 07:26:33 +02:00
parent b5b96f959c
commit f4faef6fd1
11 changed files with 489 additions and 190 deletions
+78
View File
@@ -82,6 +82,9 @@ def _wire_market_snapshot(
macro_events: list[dict[str, Any]] | None = None,
eth_pct: float = 0.10,
portfolio_eur: float | Decimal = 5000.0,
dealer_total_net_gamma: float = 12345.6,
liquidation_long_risk: str = "low",
liquidation_short_risk: str = "low",
) -> None:
"""Stub every MCP endpoint queried during the snapshot stage."""
httpx_mock.add_response(
@@ -104,6 +107,29 @@ def _wire_market_snapshot(
json={"adx": [{"value": 22.0}]},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/get_dealer_gamma_profile",
json={
"spot_price": spot,
"total_net_dealer_gamma": dealer_total_net_gamma,
"gamma_flip_level": spot * 0.99,
"strikes_analyzed": 18,
"by_strike": [],
},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-sentiment:9014/tools/get_liquidation_heatmap",
json={
"asset": "ETH",
"avg_funding_rate": funding_cross_period,
"oi_delta_pct_4h": 1.0,
"oi_delta_pct_24h": 1.0,
"long_squeeze_risk": liquidation_long_risk,
"short_squeeze_risk": liquidation_short_risk,
},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-hyperliquid:9012/tools/get_funding_rate",
json={"asset": "ETH", "current_funding_rate": funding_perp_hourly},
@@ -504,6 +530,58 @@ async def test_broker_reject_marks_position_cancelled(
assert ctx.kill_switch.is_armed() is True
@pytest.mark.asyncio
async def test_dealer_short_gamma_blocks_entry(
cfg: StrategyConfig,
runtime_paths: tuple[Path, Path],
now: datetime,
httpx_mock: HTTPXMock,
) -> None:
_wire_market_snapshot(
httpx_mock,
portfolio_eur=3500,
funding_cross_period=0.0002,
dealer_total_net_gamma=-42000.0,
)
bull_cfg = golden_config(
entry=type(cfg.entry)(
**{**cfg.entry.model_dump(), "trend_bull_threshold_pct": Decimal("0")}
)
)
ctx = _ctx(bull_cfg, runtime_paths, now)
res = await run_entry_cycle(
ctx, eur_to_usd_rate=Decimal("1.075"), now=now
)
assert res.status == "no_entry"
assert "dealer short-gamma" in (res.reason or "")
@pytest.mark.asyncio
async def test_liquidation_high_risk_blocks_entry(
cfg: StrategyConfig,
runtime_paths: tuple[Path, Path],
now: datetime,
httpx_mock: HTTPXMock,
) -> None:
_wire_market_snapshot(
httpx_mock,
portfolio_eur=3500,
funding_cross_period=0.0002,
liquidation_long_risk="high",
)
bull_cfg = golden_config(
entry=type(cfg.entry)(
**{**cfg.entry.model_dump(), "trend_bull_threshold_pct": Decimal("0")}
)
)
ctx = _ctx(bull_cfg, runtime_paths, now)
res = await run_entry_cycle(
ctx, eur_to_usd_rate=Decimal("1.075"), now=now
)
assert res.status == "no_entry"
assert "liquidation squeeze" in (res.reason or "")
@pytest.mark.asyncio
async def test_already_open_position_skips_cycle(
cfg: StrategyConfig,
+29
View File
@@ -333,6 +333,35 @@ async def test_get_positions_returns_list(httpx_mock: HTTPXMock) -> None:
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_dealer_gamma_profile_eth_parses_payload(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/get_dealer_gamma_profile",
json={
"currency": "ETH",
"spot_price": 3000.0,
"by_strike": [],
"total_net_dealer_gamma": 12345.6,
"gamma_flip_level": 2950.5,
"strikes_analyzed": 18,
},
)
snap = await _client().dealer_gamma_profile_eth()
assert snap.spot_price == Decimal("3000.0")
assert snap.total_net_dealer_gamma == Decimal("12345.6")
assert snap.gamma_flip_level == Decimal("2950.5")
assert snap.strikes_analyzed == 18
@pytest.mark.asyncio
async def test_dealer_gamma_profile_anomaly_when_total_missing(
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(json={"spot_price": 3000.0})
with pytest.raises(McpDataAnomalyError, match="missing spot_price or total"):
await _client().dealer_gamma_profile_eth()
def test_deribit_client_rejects_wrong_service() -> None:
bad = HttpToolClient(
service="macro", base_url="http://x:1", token="t", retry_max=1
+37
View File
@@ -99,6 +99,43 @@ def test_periods_table_covers_documented_venues() -> None:
}
@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",
+51 -1
View File
@@ -9,7 +9,7 @@ from decimal import Decimal
import pytest
from cerbero_bite.config import StrategyConfig, golden_config
from cerbero_bite.config import EntryConfig, StrategyConfig, golden_config
from cerbero_bite.core.entry_validator import (
EntryContext,
TrendContext,
@@ -144,6 +144,56 @@ def test_eth_holdings_at_cap_is_accepted(cfg: StrategyConfig) -> None:
# ---------------------------------------------------------------------------
def test_dealer_short_gamma_blocks_entry(cfg: StrategyConfig) -> None:
decision = validate_entry(_good_ctx(dealer_net_gamma=Decimal("-5")), cfg)
assert decision.accepted is False
assert any("dealer short-gamma" in r for r in decision.reasons)
def test_dealer_long_gamma_passes(cfg: StrategyConfig) -> None:
decision = validate_entry(_good_ctx(dealer_net_gamma=Decimal("100")), cfg)
assert decision.accepted is True
def test_dealer_gamma_none_skips_filter(cfg: StrategyConfig) -> None:
decision = validate_entry(_good_ctx(dealer_net_gamma=None), cfg)
assert decision.accepted is True
def test_liquidation_squeeze_high_blocks_entry(cfg: StrategyConfig) -> None:
decision = validate_entry(
_good_ctx(liquidation_squeeze_risk_high=True), cfg
)
assert decision.accepted is False
assert any("liquidation squeeze" in r for r in decision.reasons)
def test_liquidation_squeeze_filter_disabled_in_config(
cfg: StrategyConfig,
) -> None:
permissive = golden_config(
entry=EntryConfig(
**{**cfg.entry.model_dump(), "liquidation_filter_enabled": False}
)
)
decision = validate_entry(
_good_ctx(liquidation_squeeze_risk_high=True), permissive
)
assert decision.accepted is True
def test_dealer_gamma_filter_disabled_in_config(cfg: StrategyConfig) -> None:
permissive = golden_config(
entry=EntryConfig(
**{**cfg.entry.model_dump(), "dealer_gamma_filter_enabled": False}
)
)
decision = validate_entry(
_good_ctx(dealer_net_gamma=Decimal("-1000")), permissive
)
assert decision.accepted is True
def test_validate_entry_accumulates_all_reasons(cfg: StrategyConfig) -> None:
decision = validate_entry(
_good_ctx(