piker/tests/test_ib_trade_events.py

116 lines
3.3 KiB
Python

'''
Execution identity across IB fill and commission callbacks.
'''
from types import SimpleNamespace
from bidict import bidict
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'
)
@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'),
]