refactor: telegram + portfolio in-process (drop shared MCP)

Each bot now manages its own notification + portfolio aggregation:

* TelegramClient calls the public Bot API directly via httpx, reading
  CERBERO_BITE_TELEGRAM_BOT_TOKEN / CERBERO_BITE_TELEGRAM_CHAT_ID from
  env. No credentials → silent disabled mode.
* PortfolioClient composes DeribitClient + HyperliquidClient + the new
  MacroClient.get_asset_price/eur_usd_rate to expose equity (EUR) and
  per-asset exposure as the bot's own slice (no cross-bot view).
* mcp-telegram and mcp-portfolio removed from MCP_SERVICES / McpEndpoints
  and the cerbero-bite ping CLI; health_check no longer probes portfolio.

Docs (02/04/06/07) and docker-compose updated to reflect the new
architecture.

353/353 tests pass; ruff clean; mypy src clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 00:31:20 +02:00
parent 067f74bc89
commit abf5a140e2
26 changed files with 836 additions and 423 deletions
+32 -30
View File
@@ -154,18 +154,39 @@ def _wire_market_snapshot(
json={"events": macro_events or []},
is_reusable=True,
)
# In-process portfolio aggregator: wire the underlying exchange and
# macro endpoints so total_equity_eur and asset_pct_of_portfolio
# produce the requested ``portfolio_eur`` and ``eth_pct``.
# FX rate fixed at 1.0 → EUR amount equals USD amount in tests.
portfolio_eur_f = float(portfolio_eur)
httpx_mock.add_response(
url="http://mcp-portfolio:9018/tools/get_holdings",
url="http://mcp-macro:9013/tools/get_asset_price",
json={"ticker": "EURUSD", "price": 1.0},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/get_account_summary",
json={"equity": portfolio_eur_f, "currency": "USDC"},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-deribit:9011/tools/get_positions",
json=[
{"ticker": "AAPL", "current_value_eur": portfolio_eur_f * (1 - eth_pct)},
{"ticker": "ETH-USD", "current_value_eur": portfolio_eur_f * eth_pct},
{
"instrument_name": "ETH-15MAY26-2475-P",
"notional_usd": portfolio_eur_f * eth_pct,
}
],
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-portfolio:9018/tools/get_total_portfolio_value",
json={"total_value_eur": portfolio_eur_f},
url="http://mcp-hyperliquid:9012/tools/get_account_summary",
json={"equity": 0.0},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-hyperliquid:9012/tools/get_positions",
json=[],
is_reusable=True,
)
@@ -262,11 +283,12 @@ def _wire_combo_order(
def _wire_telegram_notify_position_opened(httpx_mock: HTTPXMock) -> None:
httpx_mock.add_response(
url="http://mcp-telegram:9017/tools/notify_position_opened",
json={"ok": True},
is_reusable=True,
)
"""No-op: Telegram is now an in-process client with disabled mode in tests.
Kept for call-site compatibility; the function used to register an MCP
notify mock but post-refactor there is no HTTP endpoint to mock when
the bot has no Telegram credentials configured.
"""
# ---------------------------------------------------------------------------
@@ -355,11 +377,6 @@ async def test_below_capital_minimum_returns_no_entry(
now: datetime,
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="http://mcp-telegram:9017/tools/notify",
json={"ok": True},
is_reusable=True,
)
# 500 EUR × 1.075 = 537 USD < 720 cfg minimum
_wire_market_snapshot(httpx_mock, portfolio_eur=500.0)
ctx = _ctx(cfg, runtime_paths, now)
@@ -377,11 +394,6 @@ async def test_macro_event_within_dte_blocks_entry(
now: datetime,
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="http://mcp-telegram:9017/tools/notify",
json={"ok": True},
is_reusable=True,
)
macro_events = [
{
"name": "FOMC",
@@ -406,11 +418,6 @@ async def test_no_bias_returns_no_entry(
now: datetime,
httpx_mock: HTTPXMock,
) -> None:
httpx_mock.add_response(
url="http://mcp-telegram:9017/tools/notify",
json={"ok": True},
is_reusable=True,
)
# Funding cross neutral (=0) and DVOL 40 → no IC, no directional;
# entry validates clean otherwise.
_wire_market_snapshot(
@@ -507,11 +514,6 @@ async def test_broker_reject_marks_position_cancelled(
},
is_reusable=True,
)
httpx_mock.add_response(
url="http://mcp-telegram:9017/tools/notify_alert",
json={"ok": True},
is_reusable=True,
)
bull_cfg = golden_config(
entry=type(cfg.entry)(
**{**cfg.entry.model_dump(), "trend_bull_threshold_pct": Decimal("0")}