Files
ccxt_with_mt5/ccxt/pro/onetrading.py
lz_db 0fab423a18 add
2025-11-16 12:31:03 +08:00

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