import asyncio import dataclasses from pathlib import Path from unittest.mock import AsyncMock, Mock import pytest from ib_async import Stock from ibkr_quant import cli from ibkr_quant.broker import AccountState, Broker from ibkr_quant.config import load_config @pytest.mark.parametrize( "mode, error, price, expected, absent", [ (3, (10197, "No market data during competing live session"), None, "Log out of the competing live market-data session", "set ib.market_data_type = 3"), (1, (10197, "No market data during competing live session"), None, "changing ib.market_data_type cannot resolve this session conflict", "set ib.market_data_type = 3"), (1, (354, "Requested market data is not subscribed"), None, "set ib.market_data_type = 3", "competing live market-data session"), (3, (354, "Requested market data is not subscribed"), None, "Delayed data is already requested", "set ib.market_data_type = 3"), (3, None, None, "no valid snapshot received", "set ib.market_data_type = 3"), (3, None, 600.0, "SPY snapshot 600.00 (requested market_data_type=3)", "no SPY price"), ], ) def test_check_explains_snapshot_result(monkeypatch, capsys, mode, error, price, expected, absent): cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml") cfg = dataclasses.replace(cfg, ib=dataclasses.replace(cfg.ib, market_data_type=mode)) async def run(): broker = Broker(cfg.ib) listeners = len(broker.ib.errorEvent) contract = Stock("SPY", "SMART", "USD", conId=756733) monkeypatch.setattr(Broker, "__aenter__", AsyncMock(return_value=broker)) monkeypatch.setattr(Broker, "__aexit__", AsyncMock(return_value=None)) monkeypatch.setattr(cli, "Broker", lambda _: broker) monkeypatch.setattr(broker, "account_state", AsyncMock(return_value=AccountState("DU123", 1000, 1000, []))) monkeypatch.setattr(broker, "open_trades", AsyncMock(return_value=[])) monkeypatch.setattr(broker, "qualify_stocks", AsyncMock(return_value=({"SPY": contract}, []))) monkeypatch.setattr(broker.ib.client, "getReqId", Mock(return_value=59)) def respond(req_id, *args): # Exercise ib_async's real request completion and errorEvent path. if error: broker.ib.wrapper.error(req_id, *error, "") else: if price is not None: broker.ib.wrapper.priceSizeTick(req_id, 4, price, 1) broker.ib.wrapper.tickSnapshotEnd(req_id) monkeypatch.setattr(broker.ib.client, "reqMktData", respond) await cli.check(cfg) assert len(broker.ib.errorEvent) == listeners asyncio.run(run()) output = capsys.readouterr().out assert "0 open orders" in output assert expected in output assert absent not in output if error: assert f"IBKR {error[0]}: {error[1]}" in output @pytest.mark.parametrize("bars, ok", [(0, False), (5, True)]) def test_gateway_probe_requires_historical_bars(monkeypatch, capsys, bars, ok): import pandas as pd cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml") broker = Broker(cfg.ib) broker.account = "DU123" frame = pd.DataFrame({"date": pd.date_range("2026-10-06", periods=bars), "symbol": "SPY", "close": 1.0}) monkeypatch.setattr(Broker, "__aenter__", AsyncMock(return_value=broker)) monkeypatch.setattr(Broker, "__aexit__", AsyncMock(return_value=None)) monkeypatch.setattr(cli, "Broker", lambda _: broker) monkeypatch.setattr(broker.ib.client, "serverVersion", Mock(return_value=178)) monkeypatch.setattr(broker, "qualify_stocks", AsyncMock(return_value=({"SPY": Stock("SPY", "SMART", "USD")}, []))) monkeypatch.setattr(broker, "daily_bars", AsyncMock(return_value=(frame, [] if bars else ["SPY"]))) if ok: asyncio.run(cli.probe_gateway(cfg, history=True)) assert "historical data OK" in capsys.readouterr().out else: with pytest.raises(SystemExit, match="historical data farm"): asyncio.run(cli.probe_gateway(cfg, history=True))