Files
ibkr-quant/tests/test_check.py
Gahow Wang e55c4ffbdb Restart the Gateway when its historical data farm stops serving
On 2026-10-12 the Gateway answered account requests but every bar request
timed out from 09:50 to 10:24 ET: make gateway reported OK, so the Gateway was
never restarted, and six retries 5 minutes apart gave up after 35 minutes. The
data came back only with the Gateway's daily restart.

ibkr-quant gateway --history also requires SPY daily bars within 30 s; the
trade job's ExecStartPre passes it, so ensure-gateway.sh restarts a Gateway
whose data farm is down. The trade job now retries every 10 minutes until
about 15:30 ET (34 starts in 6 h) instead of giving up after 35 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:27:16 -07:00

86 lines
4.1 KiB
Python

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))