Files
ibkr-quant/tests/test_order_confirmation.py
Gahow Wang a87a0c3b54 Confirm MOC orders were accepted before reporting success
rebalance used to sleep 3 s and record whatever status TWS had sent; on
2026-09-29 every order was still PendingSubmit with permId 0. Now it waits
up to 30 s for PreSubmitted/Submitted/Filled, records the final statuses in
orders.csv either way, and exits non-zero naming any rejected or
unacknowledged order so the systemd unit's failure is visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:15:53 -07:00

85 lines
3.9 KiB
Python

import asyncio
import dataclasses
import datetime as dt
from pathlib import Path
from unittest.mock import AsyncMock
import pandas as pd
import pytest
from ib_async import Order, OrderStatus, Stock, Trade, TradeLogEntry
from ibkr_quant import cli
from ibkr_quant.broker import AccountState, Broker
from ibkr_quant.config import load_config
def trade(status):
return Trade(Stock("AAA", "SMART", "USD"), Order(orderId=41, action="BUY", totalQuantity=10),
OrderStatus(status=status))
def test_confirmation_waits_for_delayed_acknowledgement():
async def run():
order = trade("PendingSubmit")
cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml")
asyncio.get_running_loop().call_later(0.02, setattr, order.orderStatus, "status", "Submitted")
await Broker(cfg.ib).confirm_orders([order], timeout=0.2)
assert order.orderStatus.status == "Submitted"
asyncio.run(run())
@pytest.mark.parametrize("status", ["PreSubmitted", "Submitted", "Filled"])
def test_confirmation_accepts_broker_acknowledged_orders(status):
cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml")
asyncio.run(Broker(cfg.ib).confirm_orders([trade(status)], timeout=0))
@pytest.mark.parametrize("status", ["Inactive", "Cancelled", "ApiCancelled", "PendingSubmit", "ApiPending"])
def test_confirmation_rejects_terminal_failure_and_missing_acknowledgement(status):
cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml")
order = trade(status)
order.log.append(TradeLogEntry(dt.datetime.now(), status, "insufficient margin", 201))
with pytest.raises(RuntimeError, match=f"AAA order 41: {status}.*insufficient margin"):
asyncio.run(Broker(cfg.ib).confirm_orders([trade("Submitted"), order], timeout=0))
def test_rebalance_records_partial_failure_and_exits_nonzero(monkeypatch, tmp_path, capsys):
cfg = load_config(Path(__file__).resolve().parents[1] / "config.toml")
cfg = dataclasses.replace(cfg, root=tmp_path, strategy=dataclasses.replace(cfg.strategy, top_n=2))
now = dt.datetime(2026, 10, 12, 10, 0, tzinfo=cli.NEW_YORK)
class Clock(dt.datetime):
@classmethod
def now(cls, tz=None):
return now
monkeypatch.setattr(cli.dt, "datetime", Clock)
idx = pd.bdate_range(end=now.date(), periods=300)
panel = pd.DataFrame({"AAA": 100.0, "BBB": 100.0}, index=idx)
monkeypatch.setattr(cli, "load_universe", lambda _: ["AAA", "BBB"])
monkeypatch.setattr(cli, "load_bars", lambda *_: (panel, panel))
monkeypatch.setattr(cli, "sessions_until_rebalance", lambda *_: 0)
monkeypatch.setattr(cli, "signal_overnight_recovery", lambda *_: panel.iloc[:-1])
async def run():
broker = Broker(cfg.ib)
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", 10000, 10000, [])))
monkeypatch.setattr(broker, "open_trades", AsyncMock(return_value=[]))
contracts = {s: Stock(s, "SMART", "USD") for s in panel.columns}
monkeypatch.setattr(broker, "qualify_stocks", AsyncMock(return_value=(contracts, [])))
def place(contract, quantity, *_):
return Trade(contract, Order(orderId=41, action="BUY", totalQuantity=quantity),
OrderStatus(status="Submitted" if contract.symbol == "AAA" else "Inactive"))
monkeypatch.setattr(broker, "place", place)
with pytest.raises(SystemExit, match="BBB order 41: Inactive"):
await cli.rebalance(cfg, submit=True)
asyncio.run(run())
orders = pd.read_csv(next((tmp_path / "runs").glob("*/orders.csv")))
assert dict(zip(orders.symbol, orders.status)) == {"AAA": "Submitted", "BBB": "Inactive"}
assert "accepted 2 orders" not in capsys.readouterr().out