# -*- coding: utf-8 -*- # PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code import ccxt.async_support from ccxt.async_support.base.ws.cache import ArrayCacheBySymbolById, ArrayCacheByTimestamp from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade from ccxt.async_support.base.ws.client import Client from typing import List from ccxt.base.errors import ExchangeError from ccxt.base.errors import NotSupported from ccxt.base.precise import Precise class onetrading(ccxt.async_support.onetrading): def describe(self) -> Any: return self.deep_extend(super(onetrading, self).describe(), { 'has': { 'ws': True, 'watchBalance': True, 'watchTicker': True, 'watchTickers': True, 'watchTrades': False, 'watchTradesForSymbols': False, 'watchMyTrades': True, 'watchOrders': True, 'watchOrderBook': True, 'watchOHLCV': True, }, 'urls': { 'api': { 'ws': 'wss://streams.onetrading.com/', }, }, 'options': { 'bp_remaining_quota': 200, 'timeframes': { '1m': { 'unit': 'MINUTES', 'period': 1, }, '5m': { 'unit': 'MINUTES', 'period': 5, }, '15m': { 'unit': 'MINUTES', 'period': 15, }, '30m': { 'unit': 'MINUTES', 'period': 30, }, '1h': { 'unit': 'HOURS', 'period': 1, }, '4h': { 'unit': 'HOURS', 'period': 4, }, '1d': { 'unit': 'DAYS', 'period': 1, }, '1w': { 'unit': 'WEEKS', 'period': 1, }, '1M': { 'unit': 'MONTHS', 'period': 1, }, }, }, 'streaming': { }, 'exceptions': { }, }) async def watch_balance(self, params={}) -> Balances: """ https://developers.bitpanda.com/exchange/#account-history-channel watch balance and get the amount of funds available for trading or funds locked in orders :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ await self.authenticate(params) url = self.urls['api']['ws'] messageHash = 'balance' subscribeHash = 'ACCOUNT_HISTORY' bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) subscribe: dict = { 'type': 'SUBSCRIBE', 'bp_remaining_quota': bpRemainingQuota, 'channels': [ { 'name': 'ACCOUNT_HISTORY', }, ], } request = self.deep_extend(subscribe, params) return await self.watch(url, messageHash, request, subscribeHash, request) def handle_balance_snapshot(self, client, message): # # snapshot # { # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", # "type": "BALANCES_SNAPSHOT", # "channel_name": "ACCOUNT_HISTORY", # "time": "2019-04-01T13:39:17.155Z", # "balances": [{ # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", # "currency_code": "BTC", # "change": "0.5", # "available": "10.0", # "locked": "1.1234567", # "sequence": 1, # "time": "2019-04-01T13:39:17.155Z" # }, # { # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", # "currency_code": "ETH", # "change": "0.5", # "available": "10.0", # "locked": "1.1234567", # "sequence": 2, # "time": "2019-04-01T13:39:17.155Z" # } # ] # } # self.balance = self.parse_balance(message) messageHash = 'balance' client.resolve(self.balance, messageHash) async def watch_ticker(self, symbol: str, params={}) -> Ticker: """ https://developers.bitpanda.com/exchange/#market-ticker-channel watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market :param str symbol: unified symbol of the market to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] subscriptionHash = 'MARKET_TICKER' messageHash = 'ticker.' + symbol request: dict = { 'type': 'SUBSCRIBE', 'channels': [ { 'name': 'MARKET_TICKER', 'price_points_mode': 'INLINE', }, ], } return await self.watch_many(messageHash, request, subscriptionHash, [symbol], params) async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ https://developers.bitpanda.com/exchange/#market-ticker-channel watches price tickers, a statistical calculation with the information for all markets or those specified. :param str symbols: unified symbols of the markets to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an array of `ticker structure ` """ await self.load_markets() symbols = self.market_symbols(symbols) if symbols is None: symbols = [] subscriptionHash = 'MARKET_TICKER' messageHash = 'tickers' request: dict = { 'type': 'SUBSCRIBE', 'channels': [ { 'name': 'MARKET_TICKER', 'price_points_mode': 'INLINE', }, ], } tickers = await self.watch_many(messageHash, request, subscriptionHash, symbols, params) return self.filter_by_array(tickers, 'symbol', symbols) def handle_ticker(self, client: Client, message): # # { # "ticker_updates": [{ # "instrument": "ETH_BTC", # "last_price": "0.053752", # "price_change": "0.000623", # "price_change_percentage": "1.17", # "high": "0.055", # "low": "0.052662", # "volume": "6.3821593247" # }], # "channel_name": "MARKET_TICKER", # "type": "MARKET_TICKER_UPDATES", # "time": "2022-06-23T16:41:00.004162Z" # } # tickers = self.safe_value(message, 'ticker_updates', []) datetime = self.safe_string(message, 'time') for i in range(0, len(tickers)): ticker = tickers[i] marketId = self.safe_string(ticker, 'instrument') symbol = self.safe_symbol(marketId) self.tickers[symbol] = self.parse_ws_ticker(ticker) timestamp = self.parse8601(datetime) self.tickers[symbol]['timestamp'] = timestamp self.tickers[symbol]['datetime'] = self.iso8601(timestamp) client.resolve(self.tickers[symbol], 'ticker.' + symbol) client.resolve(self.tickers, 'tickers') def parse_ws_ticker(self, ticker, market=None): # # { # "instrument": "ETH_BTC", # "last_price": "0.053752", # "price_change": "-0.000623", # "price_change_percentage": "-1.17", # "high": "0.055", # "low": "0.052662", # "volume": "6.3821593247" # } # marketId = self.safe_string(ticker, 'instrument') return self.safe_ticker({ 'symbol': self.safe_symbol(marketId, market), 'timestamp': None, 'datetime': None, 'high': self.safe_string(ticker, 'high'), 'low': self.safe_string(ticker, 'low'), 'bid': None, 'bidVolume': None, 'ask': None, 'askVolume': None, 'vwap': None, 'open': None, 'close': self.safe_string(ticker, 'last_price'), 'last': self.safe_string(ticker, 'last_price'), 'previousClose': None, 'change': self.safe_string(ticker, 'price_change'), 'percentage': self.safe_string(ticker, 'price_change_percentage'), 'average': None, 'baseVolume': None, 'quoteVolume': self.safe_number(ticker, 'volume'), 'info': ticker, }, market) async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ https://developers.bitpanda.com/exchange/#account-history-channel get the list of trades associated with the user :param str symbol: unified symbol of the market to fetch trades for. Use 'any' to watch all trades :param int [since]: timestamp in ms of the earliest trade to fetch :param int [limit]: the maximum amount of trades to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: a list of `trade structures ` """ await self.load_markets() messageHash = 'myTrades' if symbol is not None: market = self.market(symbol) symbol = market['symbol'] messageHash += ':' + symbol await self.authenticate(params) url = self.urls['api']['ws'] subscribeHash = 'ACCOUNT_HISTORY' bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) subscribe: dict = { 'type': 'SUBSCRIBE', 'bp_remaining_quota': bpRemainingQuota, 'channels': [ { 'name': 'ACCOUNT_HISTORY', }, ], } request = self.deep_extend(subscribe, params) trades = await self.watch(url, messageHash, request, subscribeHash, request) if self.newUpdates: limit = trades.getLimit(symbol, limit) trades = self.filter_by_symbol_since_limit(trades, symbol, since, limit) numTrades = len(trades) if numTrades == 0: return await self.watch_my_trades(symbol, since, limit, params) return trades async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: """ https://developers.bitpanda.com/exchange/#market-ticker-channel watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data :param str symbol: unified symbol of the market to fetch the order book for :param int [limit]: the maximum amount of order book entries to return :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order book structures ` indexed by market symbols """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] messageHash = 'book:' + symbol subscriptionHash = 'ORDER_BOOK' depth = 0 if limit is not None: depth = limit request: dict = { 'type': 'SUBSCRIBE', 'channels': [ { 'name': 'ORDER_BOOK', 'depth': depth, }, ], } orderbook = await self.watch_many(messageHash, request, subscriptionHash, [symbol], params) return orderbook.limit() def handle_order_book(self, client: Client, message): # # snapshot # { # "instrument_code": "ETH_BTC", # "bids": [ # ['0.053595', "4.5352"], # ... # ], # "asks": [ # ['0.055455', "0.2821"], # ... # ], # "channel_name": "ORDER_BOOK", # "type": "ORDER_BOOK_SNAPSHOT", # "time": "2022-06-23T15:38:02.196282Z" # } # # update # { # "instrument_code": "ETH_BTC", # "changes": [ # ["BUY", '0.053593', "8.0587"] # ], # "channel_name": "ORDER_BOOK", # "type": "ORDER_BOOK_UPDATE", # "time": "2022-06-23T15:38:02.751301Z" # } # type = self.safe_string(message, 'type') marketId = self.safe_string(message, 'instrument_code') symbol = self.safe_symbol(marketId) dateTime = self.safe_string(message, 'time') timestamp = self.parse8601(dateTime) channel = 'book:' + symbol orderbook = self.safe_value(self.orderbooks, symbol) if orderbook is None: orderbook = self.order_book({}) if type == 'ORDER_BOOK_SNAPSHOT': snapshot = self.parse_order_book(message, symbol, timestamp, 'bids', 'asks') orderbook.reset(snapshot) elif type == 'ORDER_BOOK_UPDATE': changes = self.safe_value(message, 'changes', []) self.handle_deltas(orderbook, changes) else: raise NotSupported(self.id + ' watchOrderBook() did not recognize message type ' + type) orderbook['nonce'] = timestamp orderbook['timestamp'] = timestamp orderbook['datetime'] = self.iso8601(timestamp) self.orderbooks[symbol] = orderbook client.resolve(orderbook, channel) def handle_delta(self, orderbook, delta): # # ['BUY', "0.053595", "0"] # bidAsk = self.parse_bid_ask(delta, 1, 2) type = self.safe_string(delta, 0) if type == 'BUY': bids = orderbook['bids'] bids.storeArray(bidAsk) elif type == 'SELL': asks = orderbook['asks'] asks.storeArray(bidAsk) else: raise NotSupported(self.id + ' watchOrderBook() received unknown change type ' + self.json(delta)) def handle_deltas(self, orderbook, deltas): # # [ # ['BUY', "0.053593", "0"], # ['SELL', "0.053698", "0"] # ] # for i in range(0, len(deltas)): self.handle_delta(orderbook, deltas[i]) async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ https://developers.bitpanda.com/exchange/#account-history-channel watches information on multiple orders made by the user :param str symbol: unified market symbol of the market orders were made in :param int [since]: the earliest time in ms to fetch orders for :param int [limit]: the maximum number of order structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.channel]: can listen to orders using ACCOUNT_HISTORY or TRADING :returns dict[]: a list of `order structures ` """ await self.load_markets() messageHash = 'orders' if symbol is not None: market = self.market(symbol) symbol = market['symbol'] messageHash += ':' + symbol await self.authenticate(params) url = self.urls['api']['ws'] subscribeHash = self.safe_string(params, 'channel', 'ACCOUNT_HISTORY') bpRemainingQuota = self.safe_integer(self.options, 'bp_remaining_quota', 200) subscribe: dict = { 'type': 'SUBSCRIBE', 'bp_remaining_quota': bpRemainingQuota, 'channels': [ { 'name': subscribeHash, }, ], } request = self.deep_extend(subscribe, params) orders = await self.watch(url, messageHash, request, subscribeHash, request) if self.newUpdates: limit = orders.getLimit(symbol, limit) orders = self.filter_by_symbol_since_limit(orders, symbol, since, limit) numOrders = len(orders) if numOrders == 0: return await self.watch_orders(symbol, since, limit, params) return orders def handle_trading(self, client: Client, message): # # { # "order_book_sequence": 892925263, # "side": "BUY", # "amount": "0.00046", # "trade_id": "d67b9b69-ab76-480f-9ba3-b33582202836", # "matched_as": "TAKER", # "matched_amount": "0.00046", # "matched_price": "22231.08", # "instrument_code": "BTC_EUR", # "order_id": "7b39f316-0a71-4bfd-adda-3062e6f0bd37", # "remaining": "0.0", # "channel_name": "TRADING", # "type": "FILL", # "time": "2022-07-21T12:41:22.883341Z" # } # # { # "status": "CANCELLED", # "order_book_sequence": 892928424, # "amount": "0.0003", # "side": "SELL", # "price": "50338.65", # "instrument_code": "BTC_EUR", # "order_id": "b3994a08-a9e8-4a79-a08b-33e3480382df", # "remaining": "0.0003", # "channel_name": "TRADING", # "type": "DONE", # "time": "2022-07-21T12:44:24.267000Z" # } # # { # "order_book_sequence": 892934476, # "side": "SELL", # "amount": "0.00051", # "price": "22349.02", # "instrument_code": "BTC_EUR", # "order_id": "1c6c585c-ec3d-4b94-9292-6c3d04a31dc8", # "remaining": "0.00051", # "channel_name": "TRADING", # "type": "BOOKED", # "time": "2022-07-21T12:50:10.093000Z" # } # if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) order = self.parse_trading_order(message) orders = self.orders orders.append(order) client.resolve(self.orders, 'orders:' + order['symbol']) client.resolve(self.orders, 'orders') def parse_trading_order(self, order, market=None): # # { # "order_book_sequence": 892925263, # "side": "BUY", # "amount": "0.00046", # "trade_id": "d67b9b69-ab76-480f-9ba3-b33582202836", # "matched_as": "TAKER", # "matched_amount": "0.00046", # "matched_price": "22231.08", # "instrument_code": "BTC_EUR", # "order_id": "7b39f316-0a71-4bfd-adda-3062e6f0bd37", # "remaining": "0.0", # "channel_name": "TRADING", # "type": "FILL", # "time": "2022-07-21T12:41:22.883341Z" # } # # { # "status": "CANCELLED", # "order_book_sequence": 892928424, # "amount": "0.0003", # "side": "SELL", # "price": "50338.65", # "instrument_code": "BTC_EUR", # "order_id": "b3994a08-a9e8-4a79-a08b-33e3480382df", # "remaining": "0.0003", # "channel_name": "TRADING", # "type": "DONE", # "time": "2022-07-21T12:44:24.267000Z" # } # # { # "order_book_sequence": 892934476, # "side": "SELL", # "amount": "0.00051", # "price": "22349.02", # "instrument_code": "BTC_EUR", # "order_id": "1c6c585c-ec3d-4b94-9292-6c3d04a31dc8", # "remaining": "0.00051", # "channel_name": "TRADING", # "type": "BOOKED", # "time": "2022-07-21T12:50:10.093000Z" # } # # { # "type":"UPDATE", # "channel_name": "TRADING", # "instrument_code": "BTC_EUR", # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", # "time": "2020-01-11T01:01:01.999Z", # "remaining": "1.23456", # "order_book_sequence": 42, # "status": "APPLIED", # "amount": "1.35756", # "amount_delta": "0.123", # "modification_id": "cc0eed67-aecc-4fb4-a625-ff3890ceb4cc" # } # tracked # { # "type": "STOP_TRACKED", # "channel_name": "TRADING", # "instrument_code": "BTC_EUR", # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", # "time": "2020-01-11T01:01:01.999Z", # "remaining": "1.23456", # "order_book_sequence": 42, # "trigger_price": "12345.67", # "current_price": "11111.11" # } # # { # "type": "STOP_TRIGGERED", # "channel_name": "TRADING", # "instrument_code": "BTC_EUR", # "order_id": "1e842f13-762a-4745-9f3b-07f1b43e7058", # "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", # "time": "2020-01-11T01:01:01.999Z", # "remaining": "1.23456", # "order_book_sequence": 42, # "price": "13333.33" # } # datetime = self.safe_string(order, 'time') marketId = self.safe_string(order, 'instrument_code') symbol = self.safe_symbol(marketId, market, '_') return self.safe_order({ 'id': self.safe_string(order, 'order_id'), 'clientOrderId': self.safe_string(order, 'client_id'), 'info': order, 'timestamp': self.parse8601(datetime), 'datetime': datetime, 'lastTradeTimestamp': None, 'symbol': symbol, 'type': None, 'timeInForce': None, 'postOnly': None, 'side': self.safe_string_lower(order, 'side'), 'price': self.safe_number_2(order, 'price', 'matched_price'), 'stopPrice': self.safe_number(order, 'trigger_price'), 'amount': self.safe_number(order, 'amount'), 'cost': None, 'average': None, 'filled': None, 'remaining': self.safe_string(order, 'remaining'), 'status': self.parse_trading_order_status(self.safe_string(order, 'status')), 'fee': None, 'trades': None, }, market) def parse_trading_order_status(self, status): statuses: dict = { 'CANCELLED': 'canceled', 'SELF_TRADE': 'rejected', 'FILLED_FULLY': 'closed', 'INSUFFICIENT_FUNDS': 'rejected', 'INSUFFICIENT_LIQUIDITY': 'rejected', 'TIME_TO_MARKET_EXCEEDED': 'rejected', 'LAST_PRICE_UNKNOWN': 'rejected', } return self.safe_string(statuses, status, status) def handle_orders(self, client: Client, message): # # snapshot # { # "account_id": "4920221a-48dc-423e-b336-bb65baccc7bd", # "orders": [{ # "order": { # "order_id": "30e2de8f-9a34-472f-bcf8-3af4b7757626", # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "instrument_code": "BTC_EUR", # "time": "2022-06-28T06:10:02.587345Z", # "side": "SELL", # "price": "19645.48", # "amount": "0.00052", # "filled_amount": "0.00052", # "type": "MARKET", # "sequence": 7633339971, # "status": "FILLED_FULLY", # "average_price": "19645.48", # "is_post_only": False, # "order_book_sequence": 866885897, # "time_last_updated": "2022-06-28T06:10:02.766983Z", # "update_modification_sequence": 866885897 # }, # "trades": [{ # "fee": { # "fee_amount": "0.01532347", # "fee_currency": "EUR", # "fee_percentage": "0.15", # "fee_group_id": "default", # "fee_type": "TAKER", # "running_trading_volume": "0.0", # "collection_type": "STANDARD" # }, # "trade": { # "trade_id": "d83e302e-0b3a-4269-aa7d-ecf007cbe577", # "order_id": "30e2de8f-9a34-472f-bcf8-3af4b7757626", # "account_holder": "49203c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "4920221a-48dc-423e-b336-bb65baccc7bd", # "amount": "0.00052", # "side": "SELL", # "instrument_code": "BTC_EUR", # "price": "19645.48", # "time": "2022-06-28T06:10:02.693246Z", # "price_tick_sequence": 0, # "sequence": 7633339971 # } # }] # }], # "channel_name": "ACCOUNT_HISTORY", # "type": "INACTIVE_ORDERS_SNAPSHOT", # "time": "2022-06-28T06:11:52.469242Z" # } # # if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) if self.myTrades is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) self.myTrades = ArrayCacheBySymbolById(limit) rawOrders = self.safe_value(message, 'orders', []) rawOrdersLength = len(rawOrders) if rawOrdersLength == 0: return orders = self.orders for i in range(0, len(rawOrders)): order = self.parse_order(rawOrders[i]) symbol = self.safe_string(order, 'symbol', '') orders.append(order) client.resolve(self.orders, 'orders:' + symbol) rawTrades = self.safe_value(rawOrders[i], 'trades', []) for ii in range(0, len(rawTrades)): trade = self.parse_trade(rawTrades[ii]) symbol = self.safe_string(trade, 'symbol', symbol) self.myTrades.append(trade) client.resolve(self.myTrades, 'myTrades:' + symbol) client.resolve(self.orders, 'orders') client.resolve(self.myTrades, 'myTrades') def handle_account_update(self, client: Client, message): # # order created # { # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", # "sequence": 7658332018, # "update": { # "type": "ORDER_CREATED", # "activity": "TRADING", # "account_holder": "43202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "order_id": "8893fd69-5ebd-496b-aaa4-269b4c18aa77", # "time": "2022-06-29T04:33:29.661257Z", # "order": { # "time_in_force": "GOOD_TILL_CANCELLED", # "is_post_only": False, # "order_id": "8892fd69-5ebd-496b-aaa4-269b4c18aa77", # "account_holder": "43202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", # "instrument_code": "BTC_EUR", # "time": "2022-06-29T04:33:29.656896Z", # "side": "SELL", # "price": "50338.65", # "amount": "0.00021", # "filled_amount": "0.0", # "type": "LIMIT" # }, # "locked": { # "currency_code": "BTC", # "amount": "0.00021", # "new_available": "0.00017", # "new_locked": "0.00021" # }, # "id": "26e9c36a-b231-4bb0-a686-aa915a2fc9e6", # "sequence": 7658332018 # }, # "channel_name": "ACCOUNT_HISTORY", # "type": "ACCOUNT_UPDATE", # "time": "2022-06-29T04:33:29.684517Z" # } # # order rejected # { # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", # "sequence": 7658332018, # "update": { # "id": "d3fe6025-5b27-4df6-a957-98b8d131cb9d", # "type": "ORDER_REJECTED", # "activity": "TRADING", # "account_id": "b355abb8-aaae-4fae-903c-c60ff74723c6", # "sequence": 0, # "timestamp": "2018-08-01T13:39:15.590Z", # "reason": "INSUFFICIENT_FUNDS", # "order_id": "6f991342-da2c-45c6-8830-8bf519cfc8cc", # "client_id": "fb497387-8223-4111-87dc-66a86f98a7cf", # "unlocked": { # "currency_code": "BTC", # "amount": "1.5", # "new_locked": "2.0", # "new_available": "1.5" # } # } # } # # order closed # { # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "sequence": 7658471216, # "update": { # "type": "ORDER_CLOSED", # "activity": "TRADING", # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "time": "2022-06-29T04:43:57.169616Z", # "order_id": "8892fd69-5ebd-496b-aaa4-269b4c18aa77", # "unlocked": { # "currency_code": "BTC", # "amount": "0.00021", # "new_available": "0.00038", # "new_locked": "0.0" # }, # "order_book_sequence": 867964191, # "id": "26c5e1d7-65ba-4a11-a661-14c0130ff484", # "sequence": 7658471216 # }, # "channel_name": "ACCOUNT_HISTORY", # "type": "ACCOUNT_UPDATE", # "time": "2022-06-29T04:43:57.182153Z" # } # # trade settled # { # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "sequence": 7658502878, # "update": { # "type": "TRADE_SETTLED", # "activity": "TRADING", # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "time": "2022-06-29T04:46:12.933091Z", # "order_id": "ad19951a-b616-401d-a062-8d0609f038a4", # "order_book_sequence": 867965579, # "filled_amount": "0.00052", # "order": { # "amount": "0.00052", # "filled_amount": "0.00052" # }, # "trade": { # "trade_id": "21039eb9-2df0-4227-be2d-0ea9b691ac66", # "order_id": "ad19951a-b616-401d-a062-8d0609f038a4", # "account_holder": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "account_id": "49202c1a-48dc-423e-b336-bb65baccc7bd", # "amount": "0.00052", # "side": "BUY", # "instrument_code": "BTC_EUR", # "price": "19309.29", # "time": "2022-06-29T04:46:12.870581Z", # "price_tick_sequence": 0 # }, # "fee": { # "fee_amount": "0.00000078", # "fee_currency": "BTC", # "fee_percentage": "0.15", # "fee_group_id": "default", # "fee_type": "TAKER", # "running_trading_volume": "0.00052", # "collection_type": "STANDARD" # }, # "spent": { # "currency_code": "EUR", # "amount": "10.0408308", # "new_available": "0.0", # "new_locked": "0.15949533" # }, # "credited": { # "currency_code": "BTC", # "amount": "0.00051922", # "new_available": "0.00089922", # "new_locked": "0.0" # }, # "unlocked": { # "currency_code": "EUR", # "amount": "0.0", # "new_available": "0.0", # "new_locked": "0.15949533" # }, # "id": "22b40199-2508-4176-8a14-d4785c933444", # "sequence": 7658502878 # }, # "channel_name": "ACCOUNT_HISTORY", # "type": "ACCOUNT_UPDATE", # "time": "2022-06-29T04:46:12.941837Z" # } # # Trade Settled with BEST fee collection enabled # { # "account_id": "49302c1a-48dc-423e-b336-bb65baccc7bd", # "sequence": 7658951984, # "update": { # "id": "70e00504-d892-456f-9aae-4da7acb36aac", # "sequence": 361792, # "order_book_sequence": 123456, # "type": "TRADE_SETTLED", # "activity": "TRADING", # "account_id": "379a12c0-4560-11e9-82fe-2b25c6f7d123", # "time": "2019-10-22T12:09:55.731Z", # "order_id": "9fcdd91c-7f6e-45f4-9956-61cddba55de5", # "client_id": "fb497387-8223-4111-87dc-66a86f98a7cf", # "order": { # "amount": "0.5", # "filled_amount": "0.5" # }, # "trade": { # "trade_id": "a828b63e-b2cb-48f0-8d99-8fc22cf98e08", # "order_id": "9fcdd91c-7f6e-45f4-9956-61cddba55de5", # "account_id": "379a12c0-4560-11e9-82fe-2b25c6f7d123", # "amount": "0.5", # "side": "BUY", # "instrument_code": "BTC_EUR", # "price": "7451.6", # "time": "2019-10-22T12:09:55.667Z" # }, # "fee": { # "fee_amount": "23.28625", # "fee_currency": "BEST", # "fee_percentage": "0.075", # "fee_group_id": "default", # "fee_type": "TAKER", # "running_trading_volume": "0.10058", # "collection_type": "BEST", # "applied_best_eur_rate": "1.04402" # }, # "spent": { # "currency_code": "EUR", # "amount": "3725.8", # "new_available": "14517885.0675703028781", # "new_locked": "2354.882" # }, # "spent_on_fees": { # "currency_code": "BEST", # "amount": "23.28625", # "new_available": "9157.31375", # "new_locked": "0.0" # }, # "credited": { # "currency_code": "BTC", # "amount": "0.5", # "new_available": "5839.89633700481", # "new_locked": "0.0" # }, # "unlocked": { # "currency_code": "EUR", # "amount": "0.15", # "new_available": "14517885.0675703028781", # "new_locked": "2354.882" # } # } # "channel_name": "ACCOUNT_HISTORY", # "type": "ACCOUNT_UPDATE", # "time": "2022-06-29T05:18:51.760338Z" # } # if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) if self.myTrades is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) self.myTrades = ArrayCacheBySymbolById(limit) symbol = None orders = self.orders update = self.safe_value(message, 'update', {}) updateType = self.safe_string(update, 'type') if updateType == 'ORDER_REJECTED' or updateType == 'ORDER_CLOSED' or updateType == 'STOP_ORDER_TRIGGERED': orderId = self.safe_string(update, 'order_id') datetime = self.safe_string_2(update, 'time', 'timestamp') previousOrderArray = self.filter_by_array(self.orders, 'id', orderId, False) previousOrder = self.safe_value(previousOrderArray, 0, {}) symbol = previousOrder['symbol'] filled = self.safe_string(update, 'filled_amount') status = self.parse_ws_order_status(updateType) if updateType == 'ORDER_CLOSED' and Precise.string_eq(filled, '0'): status = 'canceled' orderObject: dict = { 'id': orderId, 'symbol': symbol, 'status': status, 'timestamp': self.parse8601(datetime), 'datetime': datetime, } orders.append(orderObject) else: parsed = self.parse_order(update) symbol = self.safe_string(parsed, 'symbol', '') orders.append(parsed) client.resolve(self.orders, 'orders:' + symbol) client.resolve(self.orders, 'orders') # update balance balanceKeys = ['locked', 'unlocked', 'spent', 'spent_on_fees', 'credited', 'deducted'] for i in range(0, len(balanceKeys)): newBalance = self.safe_value(update, balanceKeys[i]) if newBalance is not None: self.update_balance(newBalance) client.resolve(self.balance, 'balance') # update trades if updateType == 'TRADE_SETTLED': parsed = self.parse_trade(update) symbol = self.safe_string(parsed, 'symbol', '') myTrades = self.myTrades myTrades.append(parsed) client.resolve(self.myTrades, 'myTrades:' + symbol) client.resolve(self.myTrades, 'myTrades') def parse_ws_order_status(self, status): statuses: dict = { 'ORDER_REJECTED': 'rejected', 'ORDER_CLOSED': 'closed', 'STOP_ORDER_TRIGGERED': 'triggered', } return self.safe_string(statuses, status, status) def update_balance(self, balance): # # { # "currency_code": "EUR", # "amount": "0.0", # "new_available": "0.0", # "new_locked": "0.15949533" # } # currencyId = self.safe_string(balance, 'currency_code') code = self.safe_currency_code(currencyId) account = self.account() account['free'] = self.safe_string(balance, 'new_available') account['used'] = self.safe_string(balance, 'new_locked') self.balance[code] = account self.balance = self.safe_balance(self.balance) async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]: """ https://developers.bitpanda.com/exchange/#candlesticks-channel watches historical candlestick data containing the open, high, low, and close price, and the volume of a market :param str symbol: unified symbol of the market to fetch OHLCV data for :param str timeframe: the length of time each candle represents :param int [since]: timestamp in ms of the earliest candle to fetch :param int [limit]: the maximum amount of candles to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :returns int[][]: A list of candles ordered, open, high, low, close, volume """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] marketId = market['id'] url = self.urls['api']['ws'] timeframes = self.safe_value(self.options, 'timeframes', {}) timeframeId = self.safe_value(timeframes, timeframe) if timeframeId is None: raise NotSupported(self.id + ' self interval is not supported, please provide one of the supported timeframes') messageHash = 'ohlcv.' + symbol + '.' + timeframe subscriptionHash = 'CANDLESTICKS' client = self.safe_value(self.clients, url) type = 'SUBSCRIBE' subscription = {} if client is not None: subscription = self.safe_value(client.subscriptions, subscriptionHash) if subscription is not None: ohlcvMarket = self.safe_value(subscription, marketId, {}) marketSubscribed = self.safe_bool(ohlcvMarket, timeframe, False) if not marketSubscribed: type = 'UPDATE_SUBSCRIPTION' client.subscriptions[subscriptionHash] = None else: subscription = {} subscriptionMarketId = self.safe_value(subscription, marketId) if subscriptionMarketId is None: subscription[marketId] = {} subscription[marketId][timeframe] = True properties = [] marketIds = list(subscription.keys()) for i in range(0, len(marketIds)): marketIdtimeframes = list(subscription[marketIds[i]].keys()) for ii in range(0, len(marketIdtimeframes)): marketTimeframeId = self.safe_value(timeframes, timeframe) property: dict = { 'instrument_code': marketIds[i], 'time_granularity': marketTimeframeId, } properties.append(property) request: dict = { 'type': type, 'channels': [ { 'name': 'CANDLESTICKS', 'properties': properties, }, ], } ohlcv = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash, subscription) if self.newUpdates: limit = ohlcv.getLimit(symbol, limit) return self.filter_by_since_limit(ohlcv, since, limit, 0, True) def handle_ohlcv(self, client: Client, message): # # snapshot # { # "instrument_code": "BTC_EUR", # "granularity": {unit: "MONTHS", period: 1}, # "high": "29750.81", # "low": "16764.59", # "open": "29556.02", # "close": "20164.55", # "volume": "107518944.610659", # "last_sequence": 2275507, # "channel_name": "CANDLESTICKS", # "type": "CANDLESTICK_SNAPSHOT", # "time": "2022-06-30T23:59:59.999000Z" # } # # update # { # "instrument_code": "BTC_EUR", # "granularity": { # "unit": "MINUTES", # "period": 1 # }, # "high": "20164.16", # "low": "20164.16", # "open": "20164.16", # "close": "20164.16", # "volume": "3645.2768448", # "last_sequence": 2275511, # "channel_name": "CANDLESTICKS", # "type": "CANDLESTICK", # "time": "2022-06-24T21:20:59.999000Z" # } # marketId = self.safe_string(message, 'instrument_code') symbol = self.safe_symbol(marketId) dateTime = self.safe_string(message, 'time') timeframeId = self.safe_value(message, 'granularity') timeframes = self.safe_value(self.options, 'timeframes', {}) timeframe = self.find_timeframe(timeframeId, timeframes) channel = 'ohlcv.' + symbol + '.' + timeframe parsed = [ self.parse8601(dateTime), self.safe_number(message, 'open'), self.safe_number(message, 'high'), self.safe_number(message, 'low'), self.safe_number(message, 'close'), self.safe_number(message, 'volume'), ] self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {}) stored = self.safe_value(self.ohlcvs[symbol], timeframe) if stored is None: limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) stored = ArrayCacheByTimestamp(limit) stored.append(parsed) self.ohlcvs[symbol][timeframe] = stored client.resolve(stored, channel) def find_timeframe(self, timeframe, timeframes=None): timeframes = timeframes or self.timeframes keys = list(timeframes.keys()) for i in range(0, len(keys)): key = keys[i] if timeframes[key]['unit'] == timeframe['unit'] and timeframes[key]['period'] == timeframe['period']: return key return None def handle_subscriptions(self, client: Client, message): # # { # "channels": [{ # "instrument_codes": [Array], # "depth": 0, # "name": "ORDER_BOOK" # }], # "type": "SUBSCRIPTIONS", # "time": "2022-06-23T15:36:26.948282Z" # } # return message def handle_heartbeat(self, client: Client, message): # # { # "subscription": "SYSTEM", # "channel_name": "SYSTEM", # "type": "HEARTBEAT", # "time": "2022-06-23T16:31:49.170224Z" # } # return message def handle_error_message(self, client: Client, message) -> Bool: # # { # "error": "MALFORMED_JSON", # "channel_name": "SYSTEM", # "type": "ERROR", # "time": "2022-06-23T15:38:25.470391Z" # } # raise ExchangeError(self.id + ' ' + self.json(message)) def handle_message(self, client: Client, message): error = self.safe_value(message, 'error') if error is not None: self.handle_error_message(client, message) return type = self.safe_value(message, 'type') handlers: dict = { 'ORDER_BOOK_UPDATE': self.handle_order_book, 'ORDER_BOOK_SNAPSHOT': self.handle_order_book, 'ACTIVE_ORDERS_SNAPSHOT': self.handle_orders, 'INACTIVE_ORDERS_SNAPSHOT': self.handle_orders, 'ACCOUNT_UPDATE': self.handle_account_update, 'BALANCES_SNAPSHOT': self.handle_balance_snapshot, 'SUBSCRIPTIONS': self.handle_subscriptions, 'SUBSCRIPTION_UPDATED': self.handle_subscriptions, 'PRICE_TICK': self.handle_ticker, 'PRICE_TICK_HISTORY': self.handle_subscriptions, 'HEARTBEAT': self.handle_heartbeat, 'MARKET_TICKER_UPDATES': self.handle_ticker, 'PRICE_POINT_UPDATES': self.handle_price_point_updates, 'CANDLESTICK_SNAPSHOT': self.handle_ohlcv, 'CANDLESTICK': self.handle_ohlcv, 'AUTHENTICATED': self.handle_authentication_message, 'FILL': self.handle_trading, 'DONE': self.handle_trading, 'BOOKED': self.handle_trading, 'UPDATE': self.handle_trading, 'TRACKED': self.handle_trading, 'TRIGGERED': self.handle_trading, 'STOP_TRACKED': self.handle_trading, 'STOP_TRIGGERED': self.handle_trading, } handler = self.safe_value(handlers, type) if handler is not None: handler(client, message) def handle_price_point_updates(self, client: Client, message): # # { # "channel_name": "MARKET_TICKER", # "type": "PRICE_POINT_UPDATES", # "time": "2019-03-01T10:59:59.999Z", # "price_updates": [{ # "instrument": "BTC_EUR", # "prices": [{ # "time": "2019-03-01T08:59:59.999Z", # "close_price": "3580.6" # }, # ... # ] # }, # ... # ] # } # return message def handle_authentication_message(self, client: Client, message): # # { # "channel_name": "SYSTEM", # "type": "AUTHENTICATED", # "time": "2022-06-24T20:45:25.447488Z" # } # future = self.safe_value(client.futures, 'authenticated') if future is not None: future.resolve(True) return message async def watch_many(self, messageHash, request, subscriptionHash, symbols: Strings = [], params={}): marketIds = [] numSymbols = len(symbols) if numSymbols == 0: marketIds = list(self.markets_by_id.keys()) else: marketIds = self.market_ids(symbols) url = self.urls['api']['ws'] client = self.safe_value(self.clients, url) type = 'SUBSCRIBE' subscription = {} if client is not None: subscription = self.safe_value(client.subscriptions, subscriptionHash) if subscription is not None: for i in range(0, len(marketIds)): marketId = marketIds[i] marketSubscribed = self.safe_bool(subscription, marketId, False) if not marketSubscribed: type = 'UPDATE_SUBSCRIPTION' client.subscriptions[subscriptionHash] = None else: subscription = {} for i in range(0, len(marketIds)): marketId = marketIds[i] subscription[marketId] = True request['type'] = type request['channels'][0]['instrument_codes'] = list(subscription.keys()) return await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash, subscription) async def authenticate(self, params={}): url = self.urls['api']['ws'] client = self.client(url) messageHash = 'authenticated' future = client.reusableFuture('authenticated') authenticated = self.safe_value(client.subscriptions, messageHash) if authenticated is None: self.check_required_credentials() request: dict = { 'type': 'AUTHENTICATE', 'api_token': self.apiKey, } self.watch(url, messageHash, self.extend(request, params), messageHash) return await future