bdc40929d4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
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
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_tick_uses_cache_or_subscribes():
|
|
client = MagicMock()
|
|
client.resolve_conid = AsyncMock(return_value=42)
|
|
ws = MagicMock()
|
|
ws.get_tick_snapshot = MagicMock(side_effect=[
|
|
None,
|
|
{"conid": 42, "last_price": 99.5, "bid": 99.4, "ask": 99.6,
|
|
"bid_size": 1, "ask_size": 1, "timestamp_ms": 1700000000000},
|
|
])
|
|
ws.subscribe_tick = AsyncMock()
|
|
|
|
res = await t.get_tick(
|
|
client, t.GetTickReq(symbol="AAPL"), ws=ws, timeout_s=0.05,
|
|
)
|
|
assert res["last_price"] == 99.5
|
|
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"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_place_bracket_order_calls_client_with_three_legs():
|
|
client = MagicMock()
|
|
client.resolve_conid = AsyncMock(return_value=42)
|
|
client.account_id = "DU1"
|
|
client.get_account = AsyncMock(return_value={"netliquidation": {"amount": 100000}})
|
|
client._submit_order_with_confirmation = AsyncMock(
|
|
return_value={"order_id": "OID-parent"}
|
|
)
|
|
res = await t.place_bracket_order(
|
|
client,
|
|
t.PlaceBracketOrderReq(
|
|
symbol="AAPL", side="buy", qty=1,
|
|
entry_price=150, stop_loss=145, take_profit=160,
|
|
),
|
|
creds={"max_leverage": 4},
|
|
)
|
|
assert res["order_id"] == "OID-parent"
|
|
payload = client._submit_order_with_confirmation.call_args[0][0]
|
|
assert len(payload["orders"]) == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_place_oto_partial_failure_cancels_trigger():
|
|
from cerbero_mcp.exchanges.ibkr.client import IBKRError
|
|
client = MagicMock()
|
|
client.resolve_conid = AsyncMock(return_value=42)
|
|
client.account_id = "DU1"
|
|
client._submit_order_with_confirmation = AsyncMock(
|
|
side_effect=[
|
|
{"order_id": "TRIG1"},
|
|
IBKRError("network"),
|
|
]
|
|
)
|
|
client.cancel_order = AsyncMock(return_value={"canceled": True})
|
|
with pytest.raises(IBKRError, match="IBKR_OTO_PARTIAL_FAILURE"):
|
|
await t.place_oto_order(
|
|
client,
|
|
t.PlaceOtoOrderReq(
|
|
trigger=t.OrderLeg(symbol="AAPL", side="buy", qty=1,
|
|
order_type="limit", limit_price=150),
|
|
child=t.OrderLeg(symbol="AAPL", side="sell", qty=1,
|
|
order_type="limit", limit_price=160),
|
|
),
|
|
creds={"max_leverage": 4},
|
|
)
|
|
client.cancel_order.assert_awaited_once_with("TRIG1")
|