896 lines
38 KiB
Python
896 lines
38 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 ArrayCache, ArrayCacheBySymbolById
|
|
import hashlib
|
|
from ccxt.base.types import Any, 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 AuthenticationError
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
|
|
|
|
class coinbaseexchange(ccxt.async_support.coinbaseexchange):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(coinbaseexchange, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchOHLCV': False, # missing on the exchange side
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'watchMyTradesForSymbols': True,
|
|
'watchBalance': False,
|
|
'watchStatus': False, # for now
|
|
'watchOrders': True,
|
|
'watchOrdersForSymbols': True,
|
|
'watchMyTrades': True,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': 'wss://ws-feed.exchange.coinbase.com',
|
|
},
|
|
'test': {
|
|
'ws': 'wss://ws-feed-public.sandbox.exchange.coinbase.com',
|
|
},
|
|
},
|
|
'options': {
|
|
'tradesLimit': 1000,
|
|
'ordersLimit': 1000,
|
|
'myTradesLimit': 1000,
|
|
},
|
|
})
|
|
|
|
def authenticate(self):
|
|
self.check_required_credentials()
|
|
path = '/users/self/verify'
|
|
nonce = self.nonce()
|
|
payload = str(nonce) + 'GET' + path
|
|
signature = self.hmac(self.encode(payload), self.base64_to_binary(self.secret), hashlib.sha256, 'base64')
|
|
return {
|
|
'timestamp': nonce,
|
|
'key': self.apiKey,
|
|
'signature': signature,
|
|
'passphrase': self.password,
|
|
}
|
|
|
|
async def subscribe(self, name, symbol=None, messageHashStart=None, params={}):
|
|
await self.load_markets()
|
|
market = None
|
|
messageHash = messageHashStart
|
|
productIds = []
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
messageHash += ':' + market['id']
|
|
productIds.append(market['id'])
|
|
url = self.urls['api']['ws']
|
|
if 'signature' in params:
|
|
# need to distinguish between public trades and user trades
|
|
url = url + '?'
|
|
subscribe: dict = {
|
|
'type': 'subscribe',
|
|
'product_ids': productIds,
|
|
'channels': [
|
|
name,
|
|
],
|
|
}
|
|
request = self.extend(subscribe, params)
|
|
return await self.watch(url, messageHash, request, messageHash)
|
|
|
|
async def subscribe_multiple(self, name, symbols=[], messageHashStart=None, params={}):
|
|
await self.load_markets()
|
|
market = None
|
|
symbols = self.market_symbols(symbols)
|
|
messageHashes = []
|
|
productIds = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
productIds.append(market['id'])
|
|
messageHashes.append(messageHashStart + ':' + market['symbol'])
|
|
url = self.urls['api']['ws']
|
|
if 'signature' in params:
|
|
# need to distinguish between public trades and user trades
|
|
url = url + '?'
|
|
subscribe: dict = {
|
|
'type': 'subscribe',
|
|
'product_ids': productIds,
|
|
'channels': [
|
|
name,
|
|
],
|
|
}
|
|
request = self.extend(subscribe, params)
|
|
return await self.watch_multiple(url, messageHashes, request, messageHashes)
|
|
|
|
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
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>`
|
|
"""
|
|
name = 'ticker'
|
|
return await self.subscribe(name, symbol, name, params)
|
|
|
|
async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
|
|
:param str[] [symbols]: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.channel]: the channel to subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength == 0:
|
|
raise BadSymbol(self.id + ' watchTickers requires a non-empty symbols array')
|
|
channel = 'ticker'
|
|
messageHash = 'ticker'
|
|
ticker = await self.subscribe_multiple(channel, symbols, messageHash, params)
|
|
if self.newUpdates:
|
|
result: dict = {}
|
|
result[ticker['symbol']] = ticker
|
|
return result
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
: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()
|
|
symbol = self.symbol(symbol)
|
|
name = 'matches'
|
|
trades = await self.subscribe(name, symbol, name, params)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
: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>`
|
|
"""
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength == 0:
|
|
raise BadRequest(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols')
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
name = 'matches'
|
|
trades = await self.subscribe_multiple(name, symbols, name, params)
|
|
if self.newUpdates:
|
|
first = self.safe_value(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
watches information on multiple trades made by the user
|
|
:param str symbol: unified market symbol of the market trades were made in
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' watchMyTrades() requires a symbol argument')
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
name = 'user'
|
|
messageHash = 'myTrades'
|
|
authentication = self.authenticate()
|
|
trades = await self.subscribe(name, symbol, messageHash, self.extend(params, authentication))
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def watch_my_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
watches information on multiple trades made by the user
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
await self.load_markets()
|
|
name = 'user'
|
|
messageHash = 'myTrades'
|
|
authentication = self.authenticate()
|
|
trades = await self.subscribe_multiple(name, symbols, messageHash, self.extend(params, authentication))
|
|
if self.newUpdates:
|
|
first = self.safe_value(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def watch_orders_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
watches information on multiple orders made by the user
|
|
:param str[] symbols: unified symbol of the market to fetch orders for
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
name = 'user'
|
|
messageHash = 'orders'
|
|
authentication = self.authenticate()
|
|
orders = await self.subscribe_multiple(name, symbols, messageHash, self.extend(params, authentication))
|
|
if self.newUpdates:
|
|
first = self.safe_value(orders, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = orders.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
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
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise BadSymbol(self.id + ' watchMyTrades requires a symbol')
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
name = 'user'
|
|
messageHash = 'orders'
|
|
authentication = self.authenticate()
|
|
orders = await self.subscribe(name, symbol, messageHash, self.extend(params, authentication))
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)
|
|
|
|
async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
:param str[] symbols: unified array of symbols
|
|
: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
|
|
"""
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength == 0:
|
|
raise BadRequest(self.id + ' watchOrderBookForSymbols() requires a non-empty array of symbols')
|
|
name = 'level2'
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
marketIds = self.market_ids(symbols)
|
|
messageHashes = []
|
|
for i in range(0, symbolsLength):
|
|
marketId = marketIds[i]
|
|
messageHashes.append(name + ':' + marketId)
|
|
url = self.urls['api']['ws']
|
|
subscribe: dict = {
|
|
'type': 'subscribe',
|
|
'product_ids': marketIds,
|
|
'channels': [
|
|
name,
|
|
],
|
|
}
|
|
request = self.extend(subscribe, params)
|
|
subscription: dict = {
|
|
'messageHash': name,
|
|
'symbols': symbols,
|
|
'marketIds': marketIds,
|
|
'limit': limit,
|
|
}
|
|
authentication = self.authenticate()
|
|
orderbook = await self.watch_multiple(url, messageHashes, self.extend(request, authentication), messageHashes, subscription)
|
|
return orderbook.limit()
|
|
|
|
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
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
|
|
"""
|
|
name = 'level2'
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash = name + ':' + market['id']
|
|
url = self.urls['api']['ws']
|
|
subscribe: dict = {
|
|
'type': 'subscribe',
|
|
'product_ids': [
|
|
market['id'],
|
|
],
|
|
'channels': [
|
|
name,
|
|
],
|
|
}
|
|
request = self.extend(subscribe, params)
|
|
subscription: dict = {
|
|
'messageHash': messageHash,
|
|
'symbol': symbol,
|
|
'marketId': market['id'],
|
|
'limit': limit,
|
|
}
|
|
authentication = self.authenticate()
|
|
orderbook = await self.watch(url, messageHash, self.extend(request, authentication), messageHash, subscription)
|
|
return orderbook.limit()
|
|
|
|
def handle_trade(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "match",
|
|
# "trade_id": 82047307,
|
|
# "maker_order_id": "0f358725-2134-435e-be11-753912a326e0",
|
|
# "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf",
|
|
# "side": "sell",
|
|
# "size": "0.00513192",
|
|
# "price": "9314.78",
|
|
# "product_id": "BTC-USD",
|
|
# "sequence": 12038915443,
|
|
# "time": "2020-01-31T20:03:41.158814Z"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'product_id')
|
|
if marketId is not None:
|
|
trade = self.parse_ws_trade(message)
|
|
symbol = trade['symbol']
|
|
# the exchange sends type = 'match'
|
|
# but requires 'matches' upon subscribing
|
|
# therefore we resolve 'matches' here instead of 'match'
|
|
type = 'matches'
|
|
messageHash = type + ':' + marketId
|
|
tradesArray = self.safe_value(self.trades, symbol)
|
|
if tradesArray is None:
|
|
tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
tradesArray = ArrayCache(tradesLimit)
|
|
self.trades[symbol] = tradesArray
|
|
tradesArray.append(trade)
|
|
client.resolve(tradesArray, messageHash)
|
|
return message
|
|
|
|
def handle_my_trade(self, client: Client, message):
|
|
marketId = self.safe_string(message, 'product_id')
|
|
if marketId is not None:
|
|
trade = self.parse_ws_trade(message)
|
|
type = 'myTrades'
|
|
messageHash = type + ':' + marketId
|
|
tradesArray = self.myTrades
|
|
if tradesArray is None:
|
|
limit = self.safe_integer(self.options, 'myTradesLimit', 1000)
|
|
tradesArray = ArrayCacheBySymbolById(limit)
|
|
self.myTrades = tradesArray
|
|
tradesArray.append(trade)
|
|
client.resolve(tradesArray, messageHash)
|
|
return message
|
|
|
|
def parse_ws_trade(self, trade, market=None):
|
|
#
|
|
# private trades
|
|
# {
|
|
# "type": "match",
|
|
# "trade_id": 10,
|
|
# "sequence": 50,
|
|
# "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
|
|
# "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
|
|
# "time": "2014-11-07T08:19:27.028459Z",
|
|
# "product_id": "BTC-USD",
|
|
# "size": "5.23512",
|
|
# "price": "400.23",
|
|
# "side": "sell",
|
|
# "taker_user_id: "5844eceecf7e803e259d0365",
|
|
# "user_id": "5844eceecf7e803e259d0365",
|
|
# "taker_profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352",
|
|
# "profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352",
|
|
# "taker_fee_rate": "0.005"
|
|
# }
|
|
#
|
|
# {
|
|
# "type": "match",
|
|
# "trade_id": 10,
|
|
# "sequence": 50,
|
|
# "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
|
|
# "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
|
|
# "time": "2014-11-07T08:19:27.028459Z",
|
|
# "product_id": "BTC-USD",
|
|
# "size": "5.23512",
|
|
# "price": "400.23",
|
|
# "side": "sell",
|
|
# "maker_user_id: "5844eceecf7e803e259d0365",
|
|
# "maker_id": "5844eceecf7e803e259d0365",
|
|
# "maker_profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352",
|
|
# "profile_id": "765d1549-9660-4be2-97d4-fa2d65fa3352",
|
|
# "maker_fee_rate": "0.001"
|
|
# }
|
|
#
|
|
# public trades
|
|
# {
|
|
# "type": "received",
|
|
# "time": "2014-11-07T08:19:27.028459Z",
|
|
# "product_id": "BTC-USD",
|
|
# "sequence": 10,
|
|
# "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
|
|
# "size": "1.34",
|
|
# "price": "502.1",
|
|
# "side": "buy",
|
|
# "order_type": "limit"
|
|
# }
|
|
parsed = super(coinbaseexchange, self).parse_trade(trade)
|
|
feeRate = None
|
|
isMaker = False
|
|
if 'maker_fee_rate' in trade:
|
|
isMaker = True
|
|
parsed['takerOrMaker'] = 'maker'
|
|
feeRate = self.safe_number(trade, 'maker_fee_rate')
|
|
else:
|
|
parsed['takerOrMaker'] = 'taker'
|
|
feeRate = self.safe_number(trade, 'taker_fee_rate')
|
|
# side always represents the maker side of the trade
|
|
# so if we're taker, we invert it
|
|
currentSide = parsed['side']
|
|
parsed['side'] = self.safe_string({
|
|
'buy': 'sell',
|
|
'sell': 'buy',
|
|
}, currentSide, currentSide)
|
|
idKey = 'maker_order_id' if isMaker else 'taker_order_id'
|
|
parsed['order'] = self.safe_string(trade, idKey)
|
|
market = self.market(parsed['symbol'])
|
|
feeCurrency = market['quote']
|
|
feeCost = None
|
|
if (parsed['cost'] is not None) and (feeRate is not None):
|
|
cost = self.safe_number(parsed, 'cost')
|
|
feeCost = cost * feeRate
|
|
parsed['fee'] = {
|
|
'rate': feeRate,
|
|
'cost': feeCost,
|
|
'currency': feeCurrency,
|
|
}
|
|
return parsed
|
|
|
|
def parse_ws_order_status(self, status):
|
|
statuses: dict = {
|
|
'filled': 'closed',
|
|
'canceled': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, 'open')
|
|
|
|
def handle_order(self, client: Client, message):
|
|
#
|
|
# Order is created
|
|
#
|
|
# {
|
|
# "type": "received",
|
|
# "side": "sell",
|
|
# "product_id": "BTC-USDC",
|
|
# "time": "2021-03-05T16:42:21.878177Z",
|
|
# "sequence": 5641953814,
|
|
# "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a",
|
|
# "user_id": "54fc141576dcf32596000133",
|
|
# "order_id": "11838707-bf9c-4d65-8cec-b57c9a7cab42",
|
|
# "order_type": "limit",
|
|
# "size": "0.0001",
|
|
# "price": "50000",
|
|
# "client_oid": "a317abb9-2b30-4370-ebfe-0deecb300180"
|
|
# }
|
|
#
|
|
# {
|
|
# "type": "received",
|
|
# "time": "2014-11-09T08:19:27.028459Z",
|
|
# "product_id": "BTC-USD",
|
|
# "sequence": 12,
|
|
# "order_id": "dddec984-77a8-460a-b958-66f114b0de9b",
|
|
# "funds": "3000.234",
|
|
# "side": "buy",
|
|
# "order_type": "market"
|
|
# }
|
|
#
|
|
# Order is on the order book
|
|
#
|
|
# {
|
|
# "type": "open",
|
|
# "side": "sell",
|
|
# "product_id": "BTC-USDC",
|
|
# "time": "2021-03-05T16:42:21.878177Z",
|
|
# "sequence": 5641953815,
|
|
# "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a",
|
|
# "user_id": "54fc141576dcf32596000133",
|
|
# "price": "50000",
|
|
# "order_id": "11838707-bf9c-4d65-8cec-b57c9a7cab42",
|
|
# "remaining_size": "0.0001"
|
|
# }
|
|
#
|
|
# Order is partially or completely filled
|
|
#
|
|
# {
|
|
# "type": "match",
|
|
# "side": "sell",
|
|
# "product_id": "BTC-USDC",
|
|
# "time": "2021-03-05T16:37:13.396107Z",
|
|
# "sequence": 5641897876,
|
|
# "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a",
|
|
# "user_id": "54fc141576dcf32596000133",
|
|
# "trade_id": 5455505,
|
|
# "maker_order_id": "e5f5754d-70a3-4346-95a6-209bcb503629",
|
|
# "taker_order_id": "88bf7086-7b15-40ff-8b19-ab4e08516d69",
|
|
# "size": "0.00021019",
|
|
# "price": "47338.46",
|
|
# "taker_profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a",
|
|
# "taker_user_id": "54fc141576dcf32596000133",
|
|
# "taker_fee_rate": "0.005"
|
|
# }
|
|
#
|
|
# Order is canceled / closed
|
|
#
|
|
# {
|
|
# "type": "done",
|
|
# "side": "buy",
|
|
# "product_id": "BTC-USDC",
|
|
# "time": "2021-03-05T16:37:13.396107Z",
|
|
# "sequence": 5641897877,
|
|
# "profile_id": "774ee0ce-fdda-405f-aa8d-47189a14ba0a",
|
|
# "user_id": "54fc141576dcf32596000133",
|
|
# "order_id": "88bf7086-7b15-40ff-8b19-ab4e08516d69",
|
|
# "reason": "filled"
|
|
# }
|
|
#
|
|
currentOrders = self.orders
|
|
if currentOrders is None:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
currentOrders = ArrayCacheBySymbolById(limit)
|
|
self.orders = currentOrders
|
|
type = self.safe_string(message, 'type')
|
|
marketId = self.safe_string(message, 'product_id')
|
|
if marketId is not None:
|
|
messageHash = 'orders:' + marketId
|
|
symbol = self.safe_symbol(marketId)
|
|
orderId = self.safe_string(message, 'order_id')
|
|
makerOrderId = self.safe_string(message, 'maker_order_id')
|
|
takerOrderId = self.safe_string(message, 'taker_order_id')
|
|
orders = self.orders
|
|
previousOrders = self.safe_value(orders.hashmap, symbol, {})
|
|
previousOrder = self.safe_value(previousOrders, orderId)
|
|
if previousOrder is None:
|
|
previousOrder = self.safe_value_2(previousOrders, makerOrderId, takerOrderId)
|
|
if previousOrder is None:
|
|
parsed = self.parse_ws_order(message)
|
|
orders.append(parsed)
|
|
client.resolve(orders, messageHash)
|
|
else:
|
|
sequence = self.safe_integer(message, 'sequence')
|
|
previousInfo = self.safe_value(previousOrder, 'info', {})
|
|
previousSequence = self.safe_integer(previousInfo, 'sequence')
|
|
if (previousSequence is None) or (sequence > previousSequence):
|
|
if type == 'match':
|
|
trade = self.parse_ws_trade(message)
|
|
if previousOrder['trades'] is None:
|
|
previousOrder['trades'] = []
|
|
previousOrder['trades'].append(trade)
|
|
previousOrder['lastTradeTimestamp'] = trade['timestamp']
|
|
totalCost = 0
|
|
totalAmount = 0
|
|
trades = previousOrder['trades']
|
|
for i in range(0, len(trades)):
|
|
tradeEntry = trades[i]
|
|
totalCost = self.sum(totalCost, tradeEntry['cost'])
|
|
totalAmount = self.sum(totalAmount, tradeEntry['amount'])
|
|
if totalAmount > 0:
|
|
previousOrder['average'] = totalCost / totalAmount
|
|
previousOrder['cost'] = totalCost
|
|
if previousOrder['filled'] is not None:
|
|
previousOrder['filled'] += trade['amount']
|
|
if previousOrder['amount'] is not None:
|
|
previousOrder['remaining'] = previousOrder['amount'] - previousOrder['filled']
|
|
if previousOrder['fee'] is None:
|
|
previousOrder['fee'] = {
|
|
'cost': 0,
|
|
'currency': trade['fee']['currency'],
|
|
}
|
|
if (previousOrder['fee']['cost'] is not None) and (trade['fee']['cost'] is not None):
|
|
previousOrder['fee']['cost'] = self.sum(previousOrder['fee']['cost'], trade['fee']['cost'])
|
|
# update the newUpdates count
|
|
orders.append(previousOrder)
|
|
client.resolve(orders, messageHash)
|
|
elif (type == 'received') or (type == 'done'):
|
|
info = self.extend(previousOrder['info'], message)
|
|
order = self.parse_ws_order(info)
|
|
keys = list(order.keys())
|
|
# update the reference
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
if order[key] is not None:
|
|
previousOrder[key] = order[key]
|
|
# update the newUpdates count
|
|
orders.append(previousOrder)
|
|
client.resolve(orders, messageHash)
|
|
|
|
def parse_ws_order(self, order, market=None):
|
|
id = self.safe_string(order, 'order_id')
|
|
clientOrderId = self.safe_string(order, 'client_oid')
|
|
marketId = self.safe_string(order, 'product_id')
|
|
symbol = self.safe_symbol(marketId)
|
|
side = self.safe_string(order, 'side')
|
|
price = self.safe_number(order, 'price')
|
|
amount = self.safe_number_2(order, 'size', 'funds')
|
|
time = self.safe_string(order, 'time')
|
|
timestamp = self.parse8601(time)
|
|
reason = self.safe_string(order, 'reason')
|
|
status = self.parse_ws_order_status(reason)
|
|
orderType = self.safe_string(order, 'order_type')
|
|
remaining = self.safe_number(order, 'remaining_size')
|
|
type = self.safe_string(order, 'type')
|
|
filled = None
|
|
if (amount is not None) and (remaining is not None):
|
|
filled = amount - remaining
|
|
elif type == 'received':
|
|
filled = 0
|
|
if amount is not None:
|
|
remaining = amount - filled
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'clientOrderId': clientOrderId,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'type': orderType,
|
|
'timeInForce': None,
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'stopPrice': None,
|
|
'triggerPrice': None,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'average': None,
|
|
'filled': filled,
|
|
'remaining': remaining,
|
|
'status': status,
|
|
'fee': None,
|
|
'trades': None,
|
|
})
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "ticker",
|
|
# "sequence": 12042642428,
|
|
# "product_id": "BTC-USD",
|
|
# "price": "9380.55",
|
|
# "open_24h": "9450.81000000",
|
|
# "volume_24h": "9611.79166047",
|
|
# "low_24h": "9195.49000000",
|
|
# "high_24h": "9475.19000000",
|
|
# "volume_30d": "327812.00311873",
|
|
# "best_bid": "9380.54",
|
|
# "best_ask": "9380.55",
|
|
# "side": "buy",
|
|
# "time": "2020-02-01T01:40:16.253563Z",
|
|
# "trade_id": 82062566,
|
|
# "last_size": "0.41969131"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'product_id')
|
|
if marketId is not None:
|
|
ticker = self.parse_ticker(message)
|
|
symbol = ticker['symbol']
|
|
self.tickers[symbol] = ticker
|
|
messageHash = 'ticker:' + symbol
|
|
idMessageHash = 'ticker:' + marketId
|
|
client.resolve(ticker, messageHash)
|
|
client.resolve(ticker, idMessageHash)
|
|
return message
|
|
|
|
def parse_ticker(self, ticker, market=None) -> Ticker:
|
|
#
|
|
# {
|
|
# "type": "ticker",
|
|
# "sequence": 7388547310,
|
|
# "product_id": "BTC-USDT",
|
|
# "price": "22345.67",
|
|
# "open_24h": "22308.13",
|
|
# "volume_24h": "470.21123644",
|
|
# "low_24h": "22150",
|
|
# "high_24h": "22495.15",
|
|
# "volume_30d": "25713.98401605",
|
|
# "best_bid": "22345.67",
|
|
# "best_bid_size": "0.10647825",
|
|
# "best_ask": "22349.68",
|
|
# "best_ask_size": "0.03131702",
|
|
# "side": "sell",
|
|
# "time": "2023-03-04T03:37:20.799258Z",
|
|
# "trade_id": 11586478,
|
|
# "last_size": "0.00352175"
|
|
# }
|
|
#
|
|
type = self.safe_string(ticker, 'type')
|
|
if type is None:
|
|
return super(coinbaseexchange, self).parse_ticker(ticker, market)
|
|
marketId = self.safe_string(ticker, 'product_id')
|
|
symbol = self.safe_symbol(marketId, market, '-')
|
|
timestamp = self.parse8601(self.safe_string(ticker, 'time'))
|
|
last = self.safe_string(ticker, 'price')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string(ticker, 'high_24h'),
|
|
'low': self.safe_string(ticker, 'low_24h'),
|
|
'bid': self.safe_string(ticker, 'best_bid'),
|
|
'bidVolume': self.safe_string(ticker, 'best_bid_size'),
|
|
'ask': self.safe_string(ticker, 'best_ask'),
|
|
'askVolume': self.safe_string(ticker, 'best_ask_size'),
|
|
'vwap': None,
|
|
'open': self.safe_string(ticker, 'open_24h'),
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': self.safe_string(ticker, 'volume_24h'),
|
|
'quoteVolume': None,
|
|
'info': ticker,
|
|
})
|
|
|
|
def handle_delta(self, bookside, delta):
|
|
price = self.safe_number(delta, 0)
|
|
amount = self.safe_number(delta, 1)
|
|
bookside.store(price, amount)
|
|
|
|
def handle_deltas(self, bookside, deltas):
|
|
for i in range(0, len(deltas)):
|
|
self.handle_delta(bookside, deltas[i])
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# first message(snapshot)
|
|
#
|
|
# {
|
|
# "type": "snapshot",
|
|
# "product_id": "BTC-USD",
|
|
# "bids": [
|
|
# ["10101.10", "0.45054140"]
|
|
# ],
|
|
# "asks": [
|
|
# ["10102.55", "0.57753524"]
|
|
# ]
|
|
# }
|
|
#
|
|
# subsequent updates
|
|
#
|
|
# {
|
|
# "type": "l2update",
|
|
# "product_id": "BTC-USD",
|
|
# "time": "2019-08-14T20:42:27.265Z",
|
|
# "changes": [
|
|
# ["buy", "10101.80000000", "0.162567"]
|
|
# ]
|
|
# }
|
|
#
|
|
type = self.safe_string(message, 'type')
|
|
marketId = self.safe_string(message, 'product_id')
|
|
market = self.safe_market(marketId, None, '-')
|
|
symbol = market['symbol']
|
|
name = 'level2'
|
|
messageHash = name + ':' + marketId
|
|
subscription = self.safe_value(client.subscriptions, messageHash, {})
|
|
limit = self.safe_integer(subscription, 'limit')
|
|
if type == 'snapshot':
|
|
self.orderbooks[symbol] = self.order_book({}, limit)
|
|
orderbook = self.orderbooks[symbol]
|
|
self.handle_deltas(orderbook['asks'], self.safe_value(message, 'asks', []))
|
|
self.handle_deltas(orderbook['bids'], self.safe_value(message, 'bids', []))
|
|
orderbook['timestamp'] = None
|
|
orderbook['datetime'] = None
|
|
orderbook['symbol'] = symbol
|
|
client.resolve(orderbook, messageHash)
|
|
elif type == 'l2update':
|
|
orderbook = self.orderbooks[symbol]
|
|
timestamp = self.parse8601(self.safe_string(message, 'time'))
|
|
changes = self.safe_value(message, 'changes', [])
|
|
sides: dict = {
|
|
'sell': 'asks',
|
|
'buy': 'bids',
|
|
}
|
|
for i in range(0, len(changes)):
|
|
change = changes[i]
|
|
key = self.safe_string(change, 0)
|
|
side = self.safe_string(sides, key)
|
|
price = self.safe_number(change, 1)
|
|
amount = self.safe_number(change, 2)
|
|
bookside = orderbook[side]
|
|
bookside.store(price, amount)
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
client.resolve(orderbook, messageHash)
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "subscriptions",
|
|
# "channels": [
|
|
# {
|
|
# "name": "level2",
|
|
# "product_ids": ["ETH-BTC"]
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
return message
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# {
|
|
# "type": "error",
|
|
# "message": "error message",
|
|
# /* ..."""
|
|
# }
|
|
#
|
|
# auth error
|
|
#
|
|
# {
|
|
# "type": "error",
|
|
# "message": "Authentication Failed",
|
|
# "reason": "{"message":"Invalid API Key"}"
|
|
# }
|
|
#
|
|
errMsg = self.safe_string(message, 'message')
|
|
reason = self.safe_string(message, 'reason')
|
|
try:
|
|
if errMsg == 'Authentication Failed':
|
|
raise AuthenticationError('Authentication failed: ' + reason)
|
|
else:
|
|
raise ExchangeError(self.id + ' ' + reason)
|
|
except Exception as error:
|
|
client.reject(error)
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
type = self.safe_string(message, 'type')
|
|
methods: dict = {
|
|
'snapshot': self.handle_order_book,
|
|
'l2update': self.handle_order_book,
|
|
'subscribe': self.handle_subscription_status,
|
|
'ticker': self.handle_ticker,
|
|
'received': self.handle_order,
|
|
'open': self.handle_order,
|
|
'change': self.handle_order,
|
|
'done': self.handle_order,
|
|
'error': self.handle_error_message,
|
|
}
|
|
length = len(client.url) - 0
|
|
authenticated = client.url[length - 1] == '?'
|
|
method = self.safe_value(methods, type)
|
|
if method is None:
|
|
if type == 'match':
|
|
if authenticated:
|
|
self.handle_my_trade(client, message)
|
|
self.handle_order(client, message)
|
|
else:
|
|
self.handle_trade(client, message)
|
|
else:
|
|
method(client, message)
|