Files
Cerbero-mcp/src/cerbero_mcp/routers/ibkr.py
T
2026-05-03 21:35:28 +00:00

249 lines
8.7 KiB
Python

"""Router /mcp-ibkr/* — DI per env, client e (write) creds."""
from __future__ import annotations
from typing import Literal, cast
from fastapi import APIRouter, Depends, Request
from cerbero_mcp.client_registry import ClientRegistry
from cerbero_mcp.common.audit_helpers import audit_call
from cerbero_mcp.exchanges.ibkr import tools as t
from cerbero_mcp.exchanges.ibkr.client import IBKRClient
from cerbero_mcp.exchanges.ibkr.ws import IBKRWebSocket
Environment = Literal["testnet", "mainnet"]
def get_environment(request: Request) -> Environment:
return cast(Environment, request.state.environment)
async def get_ibkr_client(
request: Request, env: Environment = Depends(get_environment),
) -> IBKRClient:
registry: ClientRegistry = request.app.state.registry
return cast(IBKRClient, await registry.get("ibkr", env))
async def get_ibkr_ws(
request: Request, env: Environment = Depends(get_environment),
) -> IBKRWebSocket:
"""Lazy-create singleton WS per env on first streaming call."""
ws_dict = getattr(request.app.state, "ibkr_ws", None)
if ws_dict is None:
ws_dict = {}
request.app.state.ibkr_ws = ws_dict
if env not in ws_dict:
client = await get_ibkr_client(request, env)
settings = request.app.state.settings
ws_url = (
settings.ibkr.ws_url_testnet if env == "testnet"
else settings.ibkr.ws_url_live
)
ws = IBKRWebSocket(
signer=client.signer,
ws_url=ws_url,
base_url=client.base_url,
max_subs=settings.ibkr.ws_max_subscriptions,
idle_timeout_s=settings.ibkr.ws_idle_timeout_s,
)
await ws.start()
ws_dict[env] = ws
return ws_dict[env]
def _build_creds(request: Request) -> dict:
settings = request.app.state.settings
return {"max_leverage": settings.ibkr.max_leverage}
def make_router() -> APIRouter:
r = APIRouter(prefix="/mcp-ibkr", tags=["ibkr"])
# === READ tools ===
@r.post("/tools/environment_info")
async def _ei(request: Request, client: IBKRClient = Depends(get_ibkr_client)):
return await t.environment_info(client, creds=_build_creds(request))
@r.post("/tools/get_account")
async def _ga(params: t.GetAccountReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_account(client, params)
@r.post("/tools/get_positions")
async def _gp(params: t.GetPositionsReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_positions(client, params)
@r.post("/tools/get_open_orders")
async def _goo(params: t.GetOpenOrdersReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_open_orders(client, params)
@r.post("/tools/get_activities")
async def _gact(params: t.GetActivitiesReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_activities(client, params)
@r.post("/tools/get_ticker")
async def _gt(params: t.GetTickerReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_ticker(client, params)
@r.post("/tools/get_bars")
async def _gb(params: t.GetBarsReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_bars(client, params)
@r.post("/tools/get_snapshot")
async def _gs(params: t.GetSnapshotReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_snapshot(client, params)
@r.post("/tools/get_option_chain")
async def _goc(params: t.GetOptionChainReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_option_chain(client, params)
@r.post("/tools/search_contracts")
async def _sc(params: t.SearchContractsReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.search_contracts(client, params)
@r.post("/tools/get_clock")
async def _gc(params: t.GetClockReq, client: IBKRClient = Depends(get_ibkr_client)):
return await t.get_clock(client, params)
# === STREAMING tools ===
@r.post("/tools/get_tick")
async def _gtk(
params: t.GetTickReq,
client: IBKRClient = Depends(get_ibkr_client),
ws: IBKRWebSocket = Depends(get_ibkr_ws),
):
return await t.get_tick(client, params, ws=ws)
@r.post("/tools/get_depth")
async def _gd(
params: t.GetDepthReq,
client: IBKRClient = Depends(get_ibkr_client),
ws: IBKRWebSocket = Depends(get_ibkr_ws),
):
return await t.get_depth(client, params, ws=ws)
@r.post("/tools/subscribe_tick")
async def _st(
params: t.SubscribeTickReq,
client: IBKRClient = Depends(get_ibkr_client),
ws: IBKRWebSocket = Depends(get_ibkr_ws),
):
return await t.subscribe_tick(client, params, ws=ws)
@r.post("/tools/unsubscribe")
async def _us(
params: t.UnsubscribeReq,
client: IBKRClient = Depends(get_ibkr_client),
ws: IBKRWebSocket = Depends(get_ibkr_ws),
):
return await t.unsubscribe(client, params, ws=ws)
# === WRITE simple ===
@r.post("/tools/place_order")
async def _po(
params: t.PlaceOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
creds = _build_creds(request)
return await audit_call(
request=request, exchange="ibkr", action="place_order",
target_field="symbol", params=params,
tool_fn=lambda: t.place_order(client, params, creds=creds),
)
@r.post("/tools/amend_order")
async def _ao(
params: t.AmendOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
return await audit_call(
request=request, exchange="ibkr", action="amend_order",
target_field="order_id", params=params,
tool_fn=lambda: t.amend_order(client, params),
)
@r.post("/tools/cancel_order")
async def _co(
params: t.CancelOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
return await audit_call(
request=request, exchange="ibkr", action="cancel_order",
target_field="order_id", params=params,
tool_fn=lambda: t.cancel_order(client, params),
)
@r.post("/tools/cancel_all_orders")
async def _cao(
params: t.CancelAllOrdersReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
return await audit_call(
request=request, exchange="ibkr", action="cancel_all_orders",
params=params, tool_fn=lambda: t.cancel_all_orders(client, params),
)
@r.post("/tools/close_position")
async def _cp(
params: t.ClosePositionReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
return await audit_call(
request=request, exchange="ibkr", action="close_position",
target_field="symbol", params=params,
tool_fn=lambda: t.close_position(client, params),
)
@r.post("/tools/close_all_positions")
async def _cap(
params: t.CloseAllPositionsReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
return await audit_call(
request=request, exchange="ibkr", action="close_all_positions",
params=params, tool_fn=lambda: t.close_all_positions(client, params),
)
# === WRITE complex ===
@r.post("/tools/place_bracket_order")
async def _pbo(
params: t.PlaceBracketOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
creds = _build_creds(request)
return await audit_call(
request=request, exchange="ibkr", action="place_bracket_order",
target_field="symbol", params=params,
tool_fn=lambda: t.place_bracket_order(client, params, creds=creds),
)
@r.post("/tools/place_oco_order")
async def _poco(
params: t.PlaceOcoOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
creds = _build_creds(request)
return await audit_call(
request=request, exchange="ibkr", action="place_oco_order",
params=params,
tool_fn=lambda: t.place_oco_order(client, params, creds=creds),
)
@r.post("/tools/place_oto_order")
async def _poto(
params: t.PlaceOtoOrderReq, request: Request,
client: IBKRClient = Depends(get_ibkr_client),
):
creds = _build_creds(request)
return await audit_call(
request=request, exchange="ibkr", action="place_oto_order",
params=params,
tool_fn=lambda: t.place_oto_order(client, params, creds=creds),
)
return r