1293 lines
54 KiB
Python
1293 lines
54 KiB
Python
# -*- 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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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
|