# piker: trading gear for hackers # Copyright (C) Tyler Goodlet (in stewardship for pikers) # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public # License as published by the Free Software Foundation, either # version 3 of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public # License along with this program. If not, see # . ''' Offline lifecycle conformance for every future brokerd adapter. ''' from decimal import Decimal import pytest from piker.clearing._codec import ProtocolError from piker.clearing._protocol import BrokerdPhase from piker.clearing._schema import ( BrokerdOpenOrder, BrokerdOpenOrdersComplete, BrokerdOrder, BrokerdPosition, BrokerdStartup, EmsStartup, Order, Status, validate_ems_startup, ) @pytest.mark.parametrize('broker', ['ib', 'kraken', 'binance']) @pytest.mark.parametrize('mode', ['live', 'paper']) def test_startup_barrier(broker: str, mode: str) -> None: ''' Every provider and paper mode shares one lifecycle contract. Prohibit order submission before completion, allow an empty snapshot, then reject a repeated completion or existing order. These fixtures qualify the protocol, not the unmigrated adapters. ''' account: str = 'paper' if mode == 'paper' else f'{broker}.test' phase: BrokerdPhase = BrokerdPhase( BrokerdStartup( broker=broker, mode=mode, accounts=(account,), ) ) request: BrokerdOrder = BrokerdOrder( oid='one', account=account, symbol='btc.spot', time_ns=1, price=Decimal('10'), size=1.0, action='buy', ) with pytest.raises(ProtocolError, match='before startup'): phase.request(request) complete: BrokerdOpenOrdersComplete = BrokerdOpenOrdersComplete( broker=broker ) assert phase.receive(complete) == complete assert phase.request(request) == request with pytest.raises(ProtocolError, match='Duplicate'): phase.receive(complete) with pytest.raises(ProtocolError, match='after startup'): phase.receive( BrokerdOpenOrder( reqid=1, time_ns=1, broker=broker, order=Order( oid='existing', account=account, symbol=f'btc.spot.{broker}', price=Decimal('10'), size=1.0, action='buy', exec_mode='live', ), ) ) def test_paper_fallback_never_opens_live_stream() -> None: ''' A backend without credentials hands control to a paper endpoint. Its fallback startup cannot masquerade as an empty live snapshot: even a syntactically valid completion event must be rejected. ''' phase: BrokerdPhase = BrokerdPhase( BrokerdStartup( broker='ib', mode='paper_fallback', ) ) with pytest.raises(ProtocolError, match='Fallback'): phase.receive(BrokerdOpenOrdersComplete(broker='ib')) assert not phase.complete @pytest.mark.parametrize( 'changes', [ {'broker': 'kraken'}, {'account': 'other'}, {'symbol': 'btc.spot.kraken'}, ], ) def test_event_ownership(changes: dict[str, str]) -> None: ''' A well-typed event may still belong to another session. Announce one broker/account, then alter a position's owner or market suffix. ``BrokerdPhase.receive()`` must reject it before EMS can publish the position. ''' phase: BrokerdPhase = BrokerdPhase( BrokerdStartup( broker='ib', mode='live', accounts=('ib.test',), ) ) position: BrokerdPosition = BrokerdPosition( broker='ib', account='ib.test', symbol='btc.spot.ib', size=1.0, avg_price=10.0, ) with pytest.raises(ProtocolError): phase.receive(position.copy(update=changes)) @pytest.mark.parametrize( 'field,value', [ ('oid', 'mismatch'), ('resp', 'closed'), ('resp', 'canceled'), ], ) def test_ems_snapshot_has_only_active_owned_dialogs( field: str, value: str, ) -> None: ''' Reconnect snapshots must not revive completed orders or associate a request with another oid. Corrupt one field in a valid snapshot and require semantic rejection after ordinary schema decoding. ''' order: Order = Order( oid='one', account='paper', symbol='btc.spot.ib', price=Decimal('10'), size=1.0, action='buy', exec_mode='live', ) status: Status = Status( time_ns=1, oid='one', resp='open', req=order ) startup: EmsStartup = EmsStartup( broker='ib', accounts=('paper',), positions=(), dialogs=(status,), ) assert validate_ems_startup(startup) == startup bad: EmsStartup = startup.copy( update={ 'dialogs': (status.copy(update={field: value}),), } ) with pytest.raises(ProtocolError): validate_ems_startup(bad) def test_ems_startup_requires_an_account() -> None: ''' EMS readiness means a live or paper account can accept requests. An empty snapshot is legal, but an empty account set cannot provide that service. Reject it even with no positions or dialogs. ''' with pytest.raises( ProtocolError, match='requires an account', ): validate_ems_startup( EmsStartup( broker='ib', accounts=(), positions=(), dialogs=(), ) )