feat(V2): IBKR router wiring + build_client + WS singleton DI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user