piker/tests/test_ib_trade_events.py

161 lines
4.9 KiB
Python

'''
Execution identity across IB fill and commission callbacks.
'''
import asyncio
from types import SimpleNamespace
from bidict import bidict
from eventkit import Event
from ib_async import (
CommissionReport,
Contract,
Execution,
Fill,
Position,
)
import pytest
import trio
from piker.brokers.ib import broker
from piker.brokers.ib.ledger import api_trades_to_ledger_entries
def make_fill(tid: str) -> Fill:
return Fill(
Contract(conId=815824267, secType='FUT', symbol='MNQ'),
Execution(
execId=tid,
acctNumber='DU_TEST',
time=1789586554.0,
),
CommissionReport(),
1789586554.0,
)
def test_execution_id_survives_empty_commission() -> None:
'''
Flattening a default CommissionReport erased Execution.execId,
causing a real fill to be skipped as an ID-less adjustment. Pass
a fill with a valid execution and empty commission through ledger
conversion and assert that its account and execution survive.
'''
entries = api_trades_to_ledger_entries(
bidict({'DU_TEST': 'ib.algopaper'}),
[make_fill('execution-1')],
)
assert entries['ib.algopaper']['execution-1']['execId'] == (
'execution-1'
)
def test_callback_preserves_fill_and_disconnect() -> None:
'''
The asyncio callback dropped commissionReportEvent's exact Fill,
forcing the Trio side to guess by contract. Exercise the actual
eventkit relay and assert it preserves the supplied object. After
that callback, emit disconnectedEvent: the bridge must raise
rather than return normally and leave its Trio owner asleep.
One asyncio checkpoint lets recv_trade_updates install handlers
before the test producer emits either event.
'''
async def main():
ib = SimpleNamespace(**{
name: Event(name)
for name in (
'orderStatusEvent', 'commissionReportEvent',
'execDetailsEvent', 'positionEvent',
'disconnectedEvent',
)
})
emitted = []
chan = SimpleNamespace(
started_nowait=lambda _: None,
send_nowait=emitted.append,
)
client = SimpleNamespace(ib=ib, inline_errors=lambda _: None)
fill = make_fill('A')
report = CommissionReport(execId='A', commission=.61)
async def produce():
await asyncio.sleep(0)
ib.commissionReportEvent.emit(None, fill, report)
ib.disconnectedEvent.emit()
producer = asyncio.create_task(produce())
with pytest.raises(ConnectionError, match='socket'):
await broker.recv_trade_updates(chan, client)
await producer
assert emitted == [('commissionReportEvent', (fill, report))]
asyncio.run(main())
@pytest.mark.parametrize('position_first', [True, False])
def test_overlapping_execution_commissions(
monkeypatch: pytest.MonkeyPatch,
position_first: bool,
) -> None:
'''
Two executions on one contract shared a single pending Fill.
A commission for execution A could therefore account execution B.
Queue both executions before either commission, delivering fees
in reverse order, and exercise positions before and after fees.
The deterministic channel order verifies every execution reaches
accounting once with its own fee, including a zero commission.
'''
fills = [make_fill('A'), make_fill('B')]
pos = Position('DU_TEST', fills[0].contract, 2, 58000)
recorded = []
async def capture(*args, ibpos, fill=None):
if fill is not None:
recorded.append((
fill.execution.execId,
fill.commissionReport.execId,
fill.commissionReport.commission,
ibpos.account,
))
monkeypatch.setattr(broker, 'emit_pp_update', capture)
monkeypatch.setattr(
broker, 'pack_position', lambda *args: ('815824267', {})
)
async def main():
send, recv = trio.open_memory_channel(10)
events = [
('execDetailsEvent', (
SimpleNamespace(contract=fill.contract), fill,
))
for fill in fills
]
events.extend([
('commissionReportEvent', (
fills[1], CommissionReport(execId='B', commission=0),
)),
('commissionReportEvent', (
fills[0], CommissionReport(execId='A', commission=.61),
)),
])
events.insert(0 if position_first else len(events), (
'positionEvent', pos,
))
async with send, recv:
for event in events:
send.send_nowait(event)
await send.aclose()
await broker.deliver_trade_events(
recv, None, {}, {}, {}, {}, None,
)
trio.run(main)
assert recorded == [
('B', 'B', 0, 'DU_TEST'),
('A', 'A', .61, 'DU_TEST'),
]