feat(V2): IBKR read tool schemas + dispatch functions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
"""IBKR tool functions: Pydantic schemas + async dispatch to client/ws."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from cerbero_mcp.exchanges.ibkr.client import IBKRClient
|
||||||
|
from cerbero_mcp.exchanges.ibkr.leverage_cap import enforce_leverage, get_max_leverage # noqa: F401
|
||||||
|
from cerbero_mcp.exchanges.ibkr.ws import IBKRWebSocket # noqa: F401
|
||||||
|
|
||||||
|
# === Schemas: reads ===
|
||||||
|
|
||||||
|
class GetAccountReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GetPositionsReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GetOpenOrdersReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GetActivitiesReq(BaseModel):
|
||||||
|
days: int = 7
|
||||||
|
|
||||||
|
class GetTickerReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
class GetBarsReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
period: str = "1d"
|
||||||
|
bar: str = "5min"
|
||||||
|
|
||||||
|
class GetSnapshotReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
class GetOptionChainReq(BaseModel):
|
||||||
|
underlying: str
|
||||||
|
expiry: str | None = None
|
||||||
|
|
||||||
|
class SearchContractsReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
sec_type: str = "STK"
|
||||||
|
|
||||||
|
class GetClockReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === Schemas: streaming ===
|
||||||
|
|
||||||
|
class GetTickReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
class GetDepthReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
rows: int = 5
|
||||||
|
exchange: str = "SMART"
|
||||||
|
|
||||||
|
class SubscribeTickReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
class UnsubscribeReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
# === Schemas: writes simple ===
|
||||||
|
|
||||||
|
class PlaceOrderReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
side: str
|
||||||
|
qty: float
|
||||||
|
order_type: str = "market"
|
||||||
|
limit_price: float | None = None
|
||||||
|
stop_price: float | None = None
|
||||||
|
tif: str = "day"
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
sec_type: str | None = None
|
||||||
|
exchange: str = "SMART"
|
||||||
|
outside_rth: bool = False
|
||||||
|
|
||||||
|
class AmendOrderReq(BaseModel):
|
||||||
|
order_id: str
|
||||||
|
qty: float | None = None
|
||||||
|
limit_price: float | None = None
|
||||||
|
stop_price: float | None = None
|
||||||
|
tif: str | None = None
|
||||||
|
|
||||||
|
class CancelOrderReq(BaseModel):
|
||||||
|
order_id: str
|
||||||
|
|
||||||
|
class CancelAllOrdersReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ClosePositionReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
qty: float | None = None
|
||||||
|
|
||||||
|
class CloseAllPositionsReq(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === Schemas: writes complex ===
|
||||||
|
|
||||||
|
class PlaceBracketOrderReq(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
side: str
|
||||||
|
qty: float
|
||||||
|
entry_price: float
|
||||||
|
stop_loss: float
|
||||||
|
take_profit: float
|
||||||
|
tif: str = "gtc"
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
exchange: str = "SMART"
|
||||||
|
|
||||||
|
class OrderLeg(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
side: str
|
||||||
|
qty: float
|
||||||
|
order_type: str = "limit"
|
||||||
|
limit_price: float | None = None
|
||||||
|
stop_price: float | None = None
|
||||||
|
tif: str = "gtc"
|
||||||
|
asset_class: str = "stocks"
|
||||||
|
|
||||||
|
class PlaceOcoOrderReq(BaseModel):
|
||||||
|
legs: list[OrderLeg]
|
||||||
|
|
||||||
|
class PlaceOtoOrderReq(BaseModel):
|
||||||
|
trigger: OrderLeg
|
||||||
|
child: OrderLeg
|
||||||
|
|
||||||
|
# === Read tools ===
|
||||||
|
|
||||||
|
async def environment_info(
|
||||||
|
client: IBKRClient, *, creds: dict, env_info: Any | None = None
|
||||||
|
) -> dict:
|
||||||
|
return {
|
||||||
|
"exchange": "ibkr",
|
||||||
|
"environment": "testnet" if client.paper else "mainnet",
|
||||||
|
"paper": client.paper,
|
||||||
|
"base_url": client.base_url,
|
||||||
|
"max_leverage": get_max_leverage(creds),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def get_account(client: IBKRClient, params: GetAccountReq) -> dict:
|
||||||
|
return await client.get_account()
|
||||||
|
|
||||||
|
async def get_positions(client: IBKRClient, params: GetPositionsReq) -> dict:
|
||||||
|
return {"positions": await client.get_positions()}
|
||||||
|
|
||||||
|
async def get_open_orders(client: IBKRClient, params: GetOpenOrdersReq) -> dict:
|
||||||
|
return {"orders": await client.get_open_orders()}
|
||||||
|
|
||||||
|
async def get_activities(client: IBKRClient, params: GetActivitiesReq) -> dict:
|
||||||
|
return {"activities": await client.get_activities(params.days)}
|
||||||
|
|
||||||
|
async def get_ticker(client: IBKRClient, params: GetTickerReq) -> dict:
|
||||||
|
return await client.get_ticker(params.symbol, params.asset_class)
|
||||||
|
|
||||||
|
async def get_bars(client: IBKRClient, params: GetBarsReq) -> dict:
|
||||||
|
return await client.get_bars(
|
||||||
|
params.symbol, params.asset_class, params.period, params.bar,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_snapshot(client: IBKRClient, params: GetSnapshotReq) -> dict:
|
||||||
|
return await client.get_ticker(params.symbol, params.asset_class)
|
||||||
|
|
||||||
|
async def get_option_chain(client: IBKRClient, params: GetOptionChainReq) -> dict:
|
||||||
|
return await client.get_option_chain(params.underlying, params.expiry)
|
||||||
|
|
||||||
|
async def search_contracts(client: IBKRClient, params: SearchContractsReq) -> dict:
|
||||||
|
return {"contracts": await client.search_contracts(params.symbol, params.sec_type)}
|
||||||
|
|
||||||
|
async def get_clock(client: IBKRClient, params: GetClockReq) -> dict:
|
||||||
|
import datetime as _dt
|
||||||
|
now = _dt.datetime.now(_dt.UTC)
|
||||||
|
return {
|
||||||
|
"timestamp": now.isoformat(),
|
||||||
|
"is_open": _dt.time(13, 30) <= now.time() <= _dt.time(20, 0)
|
||||||
|
and now.weekday() < 5,
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from cerbero_mcp.exchanges.ibkr import tools as t
|
||||||
|
|
||||||
|
|
||||||
|
def test_place_order_req_schema():
|
||||||
|
req = t.PlaceOrderReq(symbol="AAPL", side="buy", qty=1)
|
||||||
|
assert req.order_type == "market"
|
||||||
|
assert req.tif == "day"
|
||||||
|
assert req.exchange == "SMART"
|
||||||
|
|
||||||
|
|
||||||
|
def test_place_order_req_options_validates_occ():
|
||||||
|
req = t.PlaceOrderReq(
|
||||||
|
symbol="AAPL 240119C00190000", side="buy", qty=1, asset_class="options",
|
||||||
|
)
|
||||||
|
assert req.asset_class == "options"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_account_tool_calls_client():
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_account = AsyncMock(return_value={"netliquidation": {"amount": 10000}})
|
||||||
|
res = await t.get_account(client, t.GetAccountReq())
|
||||||
|
assert res["netliquidation"]["amount"] == 10000
|
||||||
Reference in New Issue
Block a user