feat(V2): IBKR simple write tools (place/amend/cancel/close)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-03 21:25:34 +00:00
parent 8914d613ec
commit 3510605fdd
2 changed files with 124 additions and 1 deletions
+83 -1
View File
@@ -4,10 +4,11 @@ from __future__ import annotations
import asyncio import asyncio
from typing import Any from typing import Any
from fastapi import HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from cerbero_mcp.exchanges.ibkr.client import _SEC_TYPE_MAP, IBKRClient, IBKRError from cerbero_mcp.exchanges.ibkr.client import _SEC_TYPE_MAP, IBKRClient, IBKRError
from cerbero_mcp.exchanges.ibkr.leverage_cap import enforce_leverage, get_max_leverage # noqa: F401 from cerbero_mcp.exchanges.ibkr.leverage_cap import get_max_leverage
from cerbero_mcp.exchanges.ibkr.ws import IBKRWebSocket from cerbero_mcp.exchanges.ibkr.ws import IBKRWebSocket
# === Schemas: reads === # === Schemas: reads ===
@@ -247,3 +248,84 @@ async def unsubscribe(
conid = await client.resolve_conid(params.symbol, sec) conid = await client.resolve_conid(params.symbol, sec)
await ws.unsubscribe(conid) await ws.unsubscribe(conid)
return {"symbol": params.symbol, "conid": conid, "unsubscribed": True} return {"symbol": params.symbol, "conid": conid, "unsubscribed": True}
# === Write tools: simple ===
async def place_order(
client: IBKRClient, params: PlaceOrderReq,
*, creds: dict, last_price: float | None = None,
) -> dict:
cap = get_max_leverage(creds)
if last_price is None:
try:
ticker = await client.get_ticker(params.symbol, params.asset_class)
last_price = ticker.get("last_price") or ticker.get("ask")
except Exception:
last_price = None
if last_price:
notional = params.qty * float(last_price)
try:
account = await client.get_account()
equity = float(
(account.get("netliquidation") or {}).get("amount") or 0
)
except Exception:
equity = 0.0
if equity > 0 and notional / equity > cap:
raise HTTPException(
status_code=403,
detail={
"error": "LEVERAGE_CAP_EXCEEDED",
"exchange": "ibkr",
"requested_ratio": notional / equity,
"max": cap,
},
)
return await client.place_order(
symbol=params.symbol,
side=params.side,
qty=params.qty,
order_type=params.order_type,
limit_price=params.limit_price,
stop_price=params.stop_price,
tif=params.tif,
asset_class=params.asset_class,
sec_type=params.sec_type,
exchange=params.exchange,
outside_rth=params.outside_rth,
)
async def amend_order(client: IBKRClient, params: AmendOrderReq) -> dict:
return await client.amend_order(
params.order_id,
qty=params.qty,
limit_price=params.limit_price,
stop_price=params.stop_price,
tif=params.tif,
)
async def cancel_order(client: IBKRClient, params: CancelOrderReq) -> dict:
return await client.cancel_order(params.order_id)
async def cancel_all_orders(
client: IBKRClient, params: CancelAllOrdersReq
) -> dict:
return {"canceled": await client.cancel_all_orders()}
async def close_position(
client: IBKRClient, params: ClosePositionReq
) -> dict:
return await client.close_position(params.symbol, params.qty)
async def close_all_positions(
client: IBKRClient, params: CloseAllPositionsReq
) -> dict:
return {"closed": await client.close_all_positions()}
+41
View File
@@ -45,3 +45,44 @@ async def test_get_tick_uses_cache_or_subscribes():
) )
assert res["last_price"] == 99.5 assert res["last_price"] == 99.5
ws.subscribe_tick.assert_awaited_once_with(42) ws.subscribe_tick.assert_awaited_once_with(42)
@pytest.mark.asyncio
async def test_place_order_enforces_leverage():
client = MagicMock()
client.get_account = AsyncMock(return_value={
"netliquidation": {"amount": 10000},
})
client.place_order = AsyncMock(return_value={"order_id": "O1"})
creds = {"max_leverage": 2}
res = await t.place_order(
client, t.PlaceOrderReq(symbol="AAPL", side="buy", qty=10),
creds=creds, last_price=100.0,
)
assert res["order_id"] == "O1"
@pytest.mark.asyncio
async def test_cancel_order_calls_client():
client = MagicMock()
client.cancel_order = AsyncMock(return_value={"order_id": "O1", "canceled": True})
res = await t.cancel_order(client, t.CancelOrderReq(order_id="O1"))
assert res["canceled"] is True
@pytest.mark.asyncio
async def test_place_order_rejects_excessive_leverage():
from fastapi import HTTPException
client = MagicMock()
client.get_account = AsyncMock(return_value={
"netliquidation": {"amount": 1000},
})
creds = {"max_leverage": 2}
# Order notional = 100*100 = 10000 vs equity 1000 → ratio 10x >> 2x cap → 403
with pytest.raises(HTTPException) as exc_info:
await t.place_order(
client, t.PlaceOrderReq(symbol="AAPL", side="buy", qty=100),
creds=creds, last_price=100.0,
)
assert exc_info.value.status_code == 403
assert exc_info.value.detail["error"] == "LEVERAGE_CAP_EXCEEDED"