"""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