async def test_handling_of_done_error_events(): """Tests handling of error messages where type is DONE""" async def handle_message(json_message): LOG.debug("ignored message %s", json_message) client = AdvancedBitpandaProWebsocketClient(None, 'test', handle_message) client.apply_trading_buffer() # Matching order would have caused a self trade => so it is never booked self_trade_prevented = '{"status": "SELF_TRADE", "instrument_code": "ETH_EUR", "order_id": ' \ '"338b7543-95d3-4cb8-a264-ed4212da7d92", "client_id": ' \ '"58672b66-fc1b-4855-add7-5365297a7213", "remaining": "60.6767", "channel_name": ' \ '"TRADING", "type": "DONE", "time": "2020-09-15T12:57:39.737Z"} ' await client.handle_message(json.loads(self_trade_prevented)) assert len(client.state.open_orders_by_order_id) == 0 # Only for market orders => when not enough funds have been locked the order fails insufficient_funds = '{"status": "INSUFFICIENT_FUNDS","instrument_code": "ETH_EUR","order_id": ' \ '"fb2d540e-6fe4-411c-8d91-7c94b94d1ae2","remaining": "1.0","channel_name": "TRADING",' \ '"type": "DONE","time": "2020-09-15T13:15:07.214Z"}' await client.handle_message(json.loads(insufficient_funds)) assert len(client.state.open_orders_by_order_id) == 0 # Only for market orders => when not enough liquidity is in the order book the market order fails insufficient_liquidity = '{"status": "INSUFFICIENT_LIQUIDITY","instrument_code": "ETH_EUR","order_id": ' \ '"80cf3fed-5766-4b5c-a378-213c5541bf5d","remaining": "80.0","channel_name": "TRADING",' \ '"type": "DONE","time": "2020-09-15T12:58:12.305Z"}' await client.handle_message(json.loads(insufficient_liquidity)) assert len(client.state.open_orders_by_order_id) == 0 # Internal system error => order was never booked time_to_market_exceeded = '{"status": "TIME_TO_MARKET_EXCEEDED","instrument_code": "ETH_EUR","order_id": ' \ '"bbf4780c-c26b-45dd-a17d-4d554a5b6e30","remaining": "34.0","channel_name": "TRADING",' \ '"type": "DONE","time": "2020-09-15T12:58:12.305Z"}' await client.handle_message(json.loads(time_to_market_exceeded)) assert len(client.state.open_orders_by_order_id) == 0
async def test_message_handling_of_trading_channel_events(): """Tests handling of messages of the trading channel""" async def handle_message(json_message): LOG.debug("ignored message %s", json_message) client = AdvancedBitpandaProWebsocketClient(None, 'test', handle_message) client.apply_trading_buffer() order_creation = '{"order":{"time_in_force":"GOOD_TILL_CANCELLED","is_post_only":false,' \ '"order_id":"c241b172-ee8d-4e1b-8900-6512c2c23579",' \ '"account_id":"379a12c0-4560-11e9-82fe-2b25c6f7d123","instrument_code":"BTC_EUR",' \ '"time":"2020-06-02T06:48:08.278Z","side":"BUY","price":"6000.0","amount":"1.0",' \ '"filled_amount":"0.0","type":"LIMIT"},"channel_name":"ORDERS","type":"ORDER_CREATED",' \ '"time":"2020-06-02T06:48:08.278Z"}' await client.handle_message(json.loads(order_creation)) assert len(client.state.open_orders_by_order_id) == 1 order_booked = '{"order_book_sequence": 1, "instrument_code": "BTC_EUR", "order_id": ' \ '"c241b172-ee8d-4e1b-8900-6512c2c23579", "remaining": "1.0", "channel_name": "TRADING", ' \ '"type": "BOOKED", "time": "2020-06-02T06:48:08.279Z"}' await client.handle_message(json.loads(order_booked)) assert len(client.state.open_orders_by_order_id) == 1 order_filled = '{"order_book_sequence": 1, "side": "BUY", "amount": "1.0", "trade_id": ' \ '"c2591a26-5f76-401f-83ec-4d20657f2db3", "matched_as": "MAKER", "matched_amount": "0.1", ' \ '"matched_price": "6000.0", "instrument_code": "BTC_EUR", "order_id": ' \ '"c241b172-ee8d-4e1b-8900-6512c2c23579", "remaining": "0.9", "channel_name": "TRADING", ' \ '"type": "FILL", "time": "2020-06-02T06:49:08.279Z"}' await client.handle_message(json.loads(order_filled)) assert len(client.state.open_orders_by_order_id) == 1 order_filled_fully = '{"status": "FILLED_FULLY", "order_book_sequence": 1, "instrument_code": "BTC_EUR", ' \ '"order_id": "c241b172-ee8d-4e1b-8900-6512c2c23579", "remaining": "0.0", "channel_name": ' \ '"TRADING", "type": "DONE", "time": "2020-06-02T06:50:08.279Z"}' await client.handle_message(json.loads(order_filled_fully)) assert len(client.state.open_orders_by_order_id) == 0
async def test_message_handling_of_stop_order_events(): """Tests handling of messages of the trading channel""" async def handle_message(json_message): LOG.debug("ignored message %s", json_message) client = AdvancedBitpandaProWebsocketClient(None, 'test', handle_message) client.apply_trading_buffer() order_creation = '{"order":{"time_in_force":"GOOD_TILL_CANCELLED","is_post_only":false,"trigger_price":"359.71",' \ '"order_id":"c02b49a0-312b-42e4-803b-5ff2179c3d5f",' \ '"account_id":"379a12c0-4560-11e9-82fe-2b25c6f7d123","instrument_code":"ETH_EUR",' \ '"time":"2020-08-07T14:21:53.691Z","side":"BUY","price":"359.71","amount":"1.0",' \ '"filled_amount":"0.0","type":"STOP"},"channel_name":"ORDERS","type":"ORDER_CREATED",' \ '"time":"2020-08-07T14:21:53.691Z"}' await client.handle_message(json.loads(order_creation)) assert len(client.state.open_orders_by_order_id) == 1 stop_order_tracked = '{"order_book_sequence": 0, "trigger_price": "359.71", "current_price": "359.78", ' \ '"instrument_code": "ETH_EUR", "order_id": "c02b49a0-312b-42e4-803b-5ff2179c3d5f", ' \ '"remaining": "1.0", "channel_name": "TRADING", "type": "TRACKED", ' \ '"time": "2020-08-07T14:21:53.692Z"} ' await client.handle_message(json.loads(stop_order_tracked)) assert len(client.state.open_orders_by_order_id) == 1 stop_order_triggered = '{"order_book_sequence": 1, "price": "359.71", "instrument_code": "ETH_EUR", "order_id": ' \ '"c02b49a0-312b-42e4-803b-5ff2179c3d5f", "remaining": "1.0", "channel_name": "TRADING", ' \ '"type": "TRIGGERED", "time": "2020-08-14T14:22:03.064Z"} ' await client.handle_message(json.loads(stop_order_triggered)) assert len(client.state.open_orders_by_order_id) == 1
async def test_handle_order_created_and_then_close(): """ Test handling of created order events""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(account_balances_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8975.828764802") assert balance.locked == Decimal("0.4") await client.handle_message(order_created_json) expected_order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") # Orders are handled through orders/trading channel assert expected_order is None # check balance balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("1.4") await client.handle_message(order_closed_json) order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") assert order is None inactive_order = client.state.inactive_orders.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") # Inactive Orders are handled through orders/trading channel assert inactive_order is None # check balance balance = client.state.balances["BTC"] assert balance.available == Decimal("8975.828764802") assert balance.locked == Decimal("0.4")
async def test_handle_account_balances(): """Test account balance snapshot handling""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(account_balances_json) balance = client.state.balances["EUR"] assert balance.available == Decimal("6606076.62363137")
async def test_verify_successful_trading_subscription(event_loop): """Handle authenticate messages""" api_token = os.environ['BP_PRO_API_TOKEN'] test_host = os.environ['TEST_HOST'] future_subscribe = event_loop.create_future() future_unsubscribe = event_loop.create_future() async def handle_message(json_message): LOG.debug("emitted event %s", json_message) if json_message["type"] == "SUBSCRIPTIONS": LOG.debug("Subscribed to: %s", json_message["channels"][0]["name"]) if "TRADING" in json_message["channels"][0]["name"]: LOG.debug("Subscribed to trading channel") future_subscribe.set_result("Success") elif json_message["type"] == "UNSUBSCRIBED" and json_message[ "channel_name"] == "TRADING": future_unsubscribe.set_result("Success") else: LOG.debug("Ignored Message") client = AdvancedBitpandaProWebsocketClient(api_token, test_host, handle_message) subscription = TradingSubscription() await client.start(Subscriptions([subscription])) LOG.info(await future_subscribe) await client.unsubscribe(Unsubscription([ChannelName.trading.value])) LOG.info(await future_unsubscribe) await client.close()
async def test_handle_inactive_orders_snapshot(): """Test handling of inactive orders snapshot""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(inactive_orders_snapshot_json) inactive_orders = client.state.last_24h_inactive_orders assert len(inactive_orders) == 4, "expected 4 orders" order = inactive_orders.get("297bd6d8-ae68-4547-b414-0bfc87d13019") assert order.instrument_code == "BTC_EUR" assert order.filled_amount == Decimal("0.2")
async def test_handle_active_orders_snapshot_multiple_instruments(): """Test active orders snapshot handling""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message( active_orders_snapshot_multiple_instruments_json) open_orders = client.state.open_orders_by_order_id assert len(open_orders) == 3 btc_eur_order = open_orders.get("ce246752-18c9-41a1-872e-759a0016b9c3") assert btc_eur_order.instrument_code == "BTC_EUR" eth_eur_order = open_orders.get("94cd6c5a-5ab8-4678-b932-7f81083d1f08") assert eth_eur_order.instrument_code == "ETH_EUR"
async def main(): when_msg_received = asyncio.get_event_loop().create_future() async def handle_message(event: json): LOG.info("%s", event) if event["type"] == "ORDER_BOOK_SNAPSHOT": when_msg_received.set_result("snapshot received...") bp_client = AdvancedBitpandaProWebsocketClient( api_token=None, wss_host="wss://streams.exchange.bitpanda.com", callback=handle_message ) order_book_subscription = OrderBookSubscription(["BTC_EUR"]) # Order book subscription without ACCOUNT_HISTORY, TRADING & ORDERS channel await bp_client.start_with(Subscriptions([order_book_subscription]), False) await when_msg_received LOG.info("asks book BTC_EUR: %s", bp_client.get_order_book("BTC_EUR").asks) LOG.info("bids BTC_EUR: %s", bp_client.get_order_book("BTC_EUR").bids) await bp_client.close()
async def main(): when_order_created = asyncio.get_event_loop().create_future() when_order_cancelled = asyncio.get_event_loop().create_future() when_order_book_snapshot_received = asyncio.get_event_loop().create_future( ) async def handle_message(event: json): LOG.info("%s", event) if event["type"] == "ORDER_BOOK_SNAPSHOT": when_order_book_snapshot_received.set_result( "snapshot received...") elif event["type"] == "ORDER_CREATED": when_order_created.set_result("created...") elif event["type"] == "ORDER_SUBMITTED_FOR_CANCELLATION": when_order_cancelled.set_result("cancelled...") # add your api token my_api_token = "eyJ..." bp_client = AdvancedBitpandaProWebsocketClient( api_token=my_api_token, wss_host="wss://streams.exchange.bitpanda.com", callback=handle_message) account_history_subscription = AccountHistorySubscription() trading_subscription = TradingSubscription() orders_subscription = OrdersSubscription() order_book_subscription = OrderBookSubscription(["BTC_EUR"]) await bp_client.start( Subscriptions([ account_history_subscription, orders_subscription, trading_subscription, order_book_subscription ])) await when_order_book_snapshot_received LOG.info("asks book BTC_EUR: %s", bp_client.get_order_book("BTC_EUR").asks) LOG.info("bids BTC_EUR: %s", bp_client.get_order_book("BTC_EUR").bids) client_id = str(uuid.uuid4()) new_order_with_client_id = CreateOrder( LimitOrder("BTC_EUR", Side.buy, 0.01, 1000.50, client_id)) LOG.info("Creating new Order with client_id: %s", new_order_with_client_id) await bp_client.create_order(new_order_with_client_id) await when_order_created LOG.info("Balances: %s", bp_client.get_state().balances) LOG.info("Open orders: %s", bp_client.get_state().open_orders_by_order_id) LOG.info("Cancel Order with client_id: %s", client_id) await bp_client.cancel_order(CancelOrderByClientId(client_id)) await when_order_cancelled LOG.info("Open orders: %s", bp_client.get_state().open_orders_by_order_id) await bp_client.close()
async def main(): when_order_created = asyncio.get_event_loop().create_future() when_order_cancelled = asyncio.get_event_loop().create_future() async def handle_message(event: json): LOG.info("%s", event) if event["type"] == "ORDER_CREATED": when_order_created.set_result("created...") elif event["type"] == "ORDER_SUBMITTED_FOR_CANCELLATION": when_order_cancelled.set_result("cancelled...") # add your api token my_api_token = "eyJ..." bp_client = AdvancedBitpandaProWebsocketClient( api_token=my_api_token, wss_host="wss://streams.exchange.bitpanda.com", callback=handle_message) # Activates ACCOUNT_HISTORY, TRADING & ORDERS channel await bp_client.start_with(None, True) client_id = str(uuid.uuid4()) new_order_with_client_id = CreateOrder( LimitOrder("BTC_EUR", Side.buy, Decimal('0.01'), Decimal('1000.50'), client_id)) LOG.info("Creating new Order with client_id: %s", new_order_with_client_id) await bp_client.create_order(new_order_with_client_id) await when_order_created LOG.info("Balances: %s", bp_client.get_state().balances) LOG.info("Open orders: %s", bp_client.get_state().open_orders_by_order_id) LOG.info("Cancel Order with client_id: %s", client_id) await bp_client.cancel_order(CancelOrderByClientId(client_id)) await when_order_cancelled LOG.info("Open orders: %s", bp_client.get_state().open_orders_by_order_id) await bp_client.close()
async def test_handle_active_orders_snapshot(): """Test active orders snapshot handling""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(active_orders_snapshot_json) open_orders = client.state.open_orders_by_order_id assert len(open_orders) == 1, "expected 1 order" order = open_orders.get("6894fe05-4071-49ca-813e-d88d3621e168") assert order.instrument_code == "BTC_EUR" assert order.order_id == "6894fe05-4071-49ca-813e-d88d3621e168" assert order.type == "LIMIT" assert order.time_in_force == "GOOD_TILL_CANCELLED" assert order.side == "SELL" assert order.price == Decimal("18500.0") assert order.remaining == Decimal("0.1") assert order.client_id == "082e0b7c-1888-4db2-b53e-208b64ae09b3"
async def test_verify_handling_of_order_books(): """test that the client handles order book messages correctly""" async def log_messages(json_message): """Callback only logging messages""" LOG.debug("message: %s", json_message) client = AdvancedBitpandaProWebsocketClient(None, 'test', log_messages) subscription = '{"channels":[{"instrument_codes":["BTC_EUR","ETH_EUR"],"depth":200,"name":"ORDER_BOOK"}],' \ '"type":"SUBSCRIPTIONS","time":"2020-07-15T12:00:00.364Z"}' await client.handle_message(json.loads(subscription)) empty_oder_book_btc_eur = client.get_order_book('BTC_EUR') assert bool(empty_oder_book_btc_eur.get_asks() ) is False, "expected empty order book for btc_eur" assert bool(empty_oder_book_btc_eur.get_bids() ) is False, "expected empty order book for btc_eur" empty_oder_book_eth_eur = client.get_order_book('ETH_EUR') assert bool(empty_oder_book_eth_eur.get_asks() ) is False, "expected empty order book for eth_eur" assert bool(empty_oder_book_eth_eur.get_bids() ) is False, "expected empty order book for eth_eur" raw_snapshot_btc_eur = '{"instrument_code":"BTC_EUR","bids":[["8860.92","0.43712"],["8858.75","0.03225"],' \ '["8856.0","0.15857"],["8855.0","0.45334"],["8852.11","0.0216"],["8850.0","0.60744"],' \ '["8845.01","3.45043"],["8845.0","3.52483"],["8838.77","0.51727"],["8835.0","0.00991"]],' \ '"asks":[["8874.23","0.36287"],["8876.4","0.0123"],["8883.0","0.43531"],["8884.0",' \ '"1.11066"],["8885.99","2.27369"],["8886.0","0.08116"],["8887.0","0.30046"],["8888.0",' \ '"0.44740"],["8890.0","0.993"],["8896.42","0.42775"]],"channel_name":"ORDER_BOOK",' \ '"type":"ORDER_BOOK_SNAPSHOT","time":"2020-07-15T12:00:00.365Z"}' await client.handle_message(json.loads(raw_snapshot_btc_eur)) oder_book_btc_eur = client.get_order_book('BTC_EUR') assert bool( oder_book_btc_eur.get_asks()) is True, "expected asks for btc_eur" assert bool( oder_book_btc_eur.get_bids()) is True, "expected bids for btc_eur" raw_snapshot_eth_eur = '{"instrument_code":"ETH_EUR","bids":[["186.3","20.4"]],' \ '"asks":[["186.58","0.36287"]],"channel_name":"ORDER_BOOK",' \ '"type":"ORDER_BOOK_SNAPSHOT","time":"2020-07-15T12:00:00.366Z"}' await client.handle_message(json.loads(raw_snapshot_eth_eur)) oder_book_eth_eur = client.get_order_book('ETH_EUR') assert bool( oder_book_eth_eur.get_asks()) is True, "expected asks for eth_eur" assert bool( oder_book_eth_eur.get_bids()) is True, "expected bids for eth_eur" unsubscribed = '{"channel_name":"ORDER_BOOK","type":"UNSUBSCRIBED","time":"2020-07-15T12:30:00.288Z"}' await client.handle_message(json.loads(unsubscribed)) assert bool(client.get_order_books()) is False
async def test_handle_out_of_order_sequenced_message(): """Test situations when an event arrives with an older sequence""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(account_balances_json) await client.handle_message(order_created_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("1.4") # an event with older sequence should be ignored, therefore no change in the balance await client.handle_message(old_seq_order_created_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("1.4") # a newer event with higher sequence should be accepted await client.handle_message(newer_seq_order_created_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8569.228764802") assert balance.locked == Decimal("2.2")
async def test_message_handling_of_orders_channel_by_using_order_id(): """Tests handling of messages of the orders channel""" async def handle_message(json_message): LOG.debug("ignored message %s", json_message) client = AdvancedBitpandaProWebsocketClient(None, 'test', handle_message) order_creation = '{"order":{"time_in_force":"GOOD_TILL_CANCELLED","is_post_only":false,' \ '"order_id":"c241b172-ee8d-4e1b-8900-6512c2c23579",' \ '"account_id":"379a12c0-4560-11e9-82fe-2b25c6f7d123","instrument_code":"BTC_EUR",' \ '"time":"2020-06-02T06:48:08.278Z","side":"BUY","price":"6000.0","amount":"1.0",' \ '"filled_amount":"0.0","type":"LIMIT"},"channel_name":"ORDERS","type":"ORDER_CREATED",' \ '"time":"2020-06-02T06:48:08.278Z"} ' await client.handle_message(json.loads(order_creation)) assert len(client.state.open_orders_by_client_id) == 0 assert len(client.state.open_orders_by_order_id) == 1 order_cancellation = '{"order_id":"c241b172-ee8d-4e1b-8900-6512c2c23579","channel_name":"ORDERS",' \ '"type":"ORDER_SUBMITTED_FOR_CANCELLATION","time":"2020-06-02T06:48:08.381Z"} ' await client.handle_message(json.loads(order_cancellation)) assert len(client.state.open_orders_by_client_id) == 0 # Open order is removed from store on trading channel update assert len(client.state.open_orders_by_order_id) == 1
async def test_withdrawal_of_funds(): """Verify correct balance after withdrawal""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(account_balances_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8975.828764802") assert balance.locked == Decimal("0.4") balance = client.state.balances["EUR"] assert balance.available == Decimal("6606076.62363137") assert balance.locked == Decimal("0.0") # Change in BTC balance after 0.22 BTC withdrawal await client.handle_message(account_balance_withdrawal) balance = client.state.balances["BTC"] assert balance.available == Decimal("8975.608764802") assert balance.locked == Decimal("0.12") # No change in EUR balance balance = client.state.balances["EUR"] assert balance.available == Decimal("6606076.62363137") assert balance.locked == Decimal("0.0")
async def test_handle_trade_settled_updates(): """Test trade settlement events""" client = AdvancedBitpandaProWebsocketClient("irrelevant", "irrelevant", log_messages) await client.handle_message(account_balances_json) balance = client.state.balances["BTC"] assert balance.available == Decimal("8975.828764802") assert balance.locked == Decimal("0.4") # ------- Order created ---------- await client.handle_message(order_created_json) expected_order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") # Orders are handled through orders/trading channel assert expected_order is None # On order channel update the order is in the store await client.handle_message(order_created_orders_channel_json) expected_order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") assert expected_order is not None assert expected_order.remaining == Decimal("1.0") assert expected_order.order_id == "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c" assert expected_order.price == "8500.0" # check balance balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("1.4") # ------- half of order settled ---------- await client.handle_message(trade_settled_partially_json) # check balance again, partially filled balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("0.9") # order is still part of open orders expected_order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") assert expected_order is not None assert expected_order.remaining == Decimal("1.0") assert expected_order.order_id == "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c" assert expected_order.price == "8500.0" # ------- fully settled order ---------- await client.handle_message(trade_settled_json) # check balance again, not locked anymore balance = client.state.balances["BTC"] assert balance.available == Decimal("8974.828764802") assert balance.locked == Decimal("0.4") # order is completed, on update from trading channel the store is updated client.apply_trading_buffer() await client.handle_message(trade_settled_order_done_json) expected_order = client.state.open_orders_by_order_id.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") assert expected_order is None inactive_order = client.state.inactive_orders.get( "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c") assert inactive_order is not None assert inactive_order.order_id == "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c" assert inactive_order.remaining == Decimal("0.0") assert inactive_order.order_id == "65ecb524-4a7f-4b22-aa44-ec0b38d3db9c" assert inactive_order.price == "8500.0"