''' 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'), ]