556 lines
22 KiB
Python
556 lines
22 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
|
|
from ccxt.base.types import Any, Bool, Int, Order, OrderBook, Str, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class bitstamp(ccxt.async_support.bitstamp):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bitstamp, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchOrderBook': True,
|
|
'watchOrders': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': False,
|
|
'watchOHLCV': False,
|
|
'watchTicker': False,
|
|
'watchTickers': False,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': 'wss://ws.bitstamp.net',
|
|
},
|
|
},
|
|
'options': {
|
|
'expiresIn': '',
|
|
'userId': '',
|
|
'wsSessionToken': '',
|
|
'watchOrderBook': {
|
|
'snapshotDelay': 6,
|
|
'snapshotMaxRetries': 3,
|
|
},
|
|
'tradesLimit': 1000,
|
|
'OHLCVLimit': 1000,
|
|
},
|
|
'exceptions': {
|
|
'exact': {
|
|
'4009': AuthenticationError,
|
|
},
|
|
},
|
|
})
|
|
|
|
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
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash = 'orderbook:' + symbol
|
|
channel = 'diff_order_book_' + market['id']
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'event': 'bts:subscribe',
|
|
'data': {
|
|
'channel': channel,
|
|
},
|
|
}
|
|
message = self.extend(request, params)
|
|
orderbook = await self.watch(url, messageHash, message, messageHash)
|
|
return orderbook.limit()
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# initial snapshot is fetched with ccxt's fetchOrderBook
|
|
# the feed does not include a snapshot, just the deltas
|
|
#
|
|
# {
|
|
# "data": {
|
|
# "timestamp": "1583656800",
|
|
# "microtimestamp": "1583656800237527",
|
|
# "bids": [
|
|
# ["8732.02", "0.00002478", "1207590500704256"],
|
|
# ["8729.62", "0.01600000", "1207590502350849"],
|
|
# ["8727.22", "0.01800000", "1207590504296448"],
|
|
# ],
|
|
# "asks": [
|
|
# ["8735.67", "2.00000000", "1207590693249024"],
|
|
# ["8735.67", "0.01700000", "1207590693634048"],
|
|
# ["8735.68", "1.53294500", "1207590692048896"],
|
|
# ],
|
|
# },
|
|
# "event": "data",
|
|
# "channel": "diff_order_book_btcusd"
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
parts = channel.split('_')
|
|
marketId = self.safe_string(parts, 3)
|
|
symbol = self.safe_symbol(marketId)
|
|
storedOrderBook = self.safe_value(self.orderbooks, symbol)
|
|
nonce = self.safe_value(storedOrderBook, 'nonce')
|
|
delta = self.safe_value(message, 'data')
|
|
deltaNonce = self.safe_integer(delta, 'microtimestamp')
|
|
messageHash = 'orderbook:' + symbol
|
|
if nonce is None:
|
|
cacheLength = len(storedOrderBook.cache)
|
|
# the rest API is very delayed
|
|
# usually it takes at least 4-5 deltas to resolve
|
|
snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 6)
|
|
if cacheLength == snapshotDelay:
|
|
self.spawn(self.load_order_book, client, messageHash, symbol, None, {})
|
|
storedOrderBook.cache.append(delta)
|
|
return
|
|
elif nonce >= deltaNonce:
|
|
return
|
|
self.handle_delta(storedOrderBook, delta)
|
|
client.resolve(storedOrderBook, messageHash)
|
|
|
|
def handle_delta(self, orderbook, delta):
|
|
timestamp = self.safe_timestamp(delta, 'timestamp')
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
orderbook['nonce'] = self.safe_integer(delta, 'microtimestamp')
|
|
bids = self.safe_value(delta, 'bids', [])
|
|
asks = self.safe_value(delta, 'asks', [])
|
|
storedBids = orderbook['bids']
|
|
storedAsks = orderbook['asks']
|
|
self.handle_bid_asks(storedBids, bids)
|
|
self.handle_bid_asks(storedAsks, asks)
|
|
|
|
def handle_bid_asks(self, bookSide, bidAsks):
|
|
for i in range(0, len(bidAsks)):
|
|
bidAsk = self.parse_bid_ask(bidAsks[i])
|
|
bookSide.storeArray(bidAsk)
|
|
|
|
def get_cache_index(self, orderbook, deltas):
|
|
# we will consider it a fail
|
|
firstElement = deltas[0]
|
|
firstElementNonce = self.safe_integer(firstElement, 'microtimestamp')
|
|
nonce = self.safe_integer(orderbook, 'nonce')
|
|
if nonce < firstElementNonce:
|
|
return -1
|
|
for i in range(0, len(deltas)):
|
|
delta = deltas[i]
|
|
deltaNonce = self.safe_integer(delta, 'microtimestamp')
|
|
if deltaNonce == nonce:
|
|
return i + 1
|
|
return len(deltas)
|
|
|
|
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()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash = 'trades:' + symbol
|
|
url = self.urls['api']['ws']
|
|
channel = 'live_trades_' + market['id']
|
|
request: dict = {
|
|
'event': 'bts:subscribe',
|
|
'data': {
|
|
'channel': channel,
|
|
},
|
|
}
|
|
message = self.extend(request, params)
|
|
trades = await self.watch(url, messageHash, message, messageHash)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
def parse_ws_trade(self, trade, market=None):
|
|
#
|
|
# {
|
|
# "buy_order_id": 1211625836466176,
|
|
# "amount_str": "1.08000000",
|
|
# "timestamp": "1584642064",
|
|
# "microtimestamp": "1584642064685000",
|
|
# "id": 108637852,
|
|
# "amount": 1.08,
|
|
# "sell_order_id": 1211625840754689,
|
|
# "price_str": "6294.77",
|
|
# "type": 1,
|
|
# "price": 6294.77
|
|
# }
|
|
#
|
|
microtimestamp = self.safe_integer(trade, 'microtimestamp')
|
|
id = self.safe_string(trade, 'id')
|
|
timestamp = self.parse_to_int(microtimestamp / 1000)
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'amount')
|
|
symbol = market['symbol']
|
|
sideRaw = self.safe_integer(trade, 'type')
|
|
side = 'buy' if (sideRaw == 0) else 'sell'
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'order': None,
|
|
'type': None,
|
|
'takerOrMaker': None,
|
|
'side': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'fee': None,
|
|
}, market)
|
|
|
|
def handle_trade(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "data": {
|
|
# "buy_order_id": 1207733769326592,
|
|
# "amount_str": "0.14406384",
|
|
# "timestamp": "1583691851",
|
|
# "microtimestamp": "1583691851934000",
|
|
# "id": 106833903,
|
|
# "amount": 0.14406384,
|
|
# "sell_order_id": 1207733765476352,
|
|
# "price_str": "8302.92",
|
|
# "type": 0,
|
|
# "price": 8302.92
|
|
# },
|
|
# "event": "trade",
|
|
# "channel": "live_trades_btcusd"
|
|
# }
|
|
#
|
|
# the trade streams push raw trade information in real-time
|
|
# each trade has a unique buyer and seller
|
|
channel = self.safe_string(message, 'channel')
|
|
parts = channel.split('_')
|
|
marketId = self.safe_string(parts, 2)
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
messageHash = 'trades:' + symbol
|
|
data = self.safe_value(message, 'data')
|
|
trade = self.parse_ws_trade(data, market)
|
|
tradesArray = self.safe_value(self.trades, symbol)
|
|
if tradesArray is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
tradesArray = ArrayCache(limit)
|
|
self.trades[symbol] = tradesArray
|
|
tradesArray.append(trade)
|
|
client.resolve(tradesArray, messageHash)
|
|
|
|
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 ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
channel = 'private-my_orders'
|
|
messageHash = channel + '_' + market['id']
|
|
subscription: dict = {
|
|
'symbol': symbol,
|
|
'limit': limit,
|
|
'type': channel,
|
|
'params': params,
|
|
}
|
|
orders = await self.subscribe_private(subscription, messageHash, params)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)
|
|
|
|
def handle_orders(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "data":{
|
|
# "id":"1463471322288128",
|
|
# "id_str":"1463471322288128",
|
|
# "order_type":1,
|
|
# "datetime":"1646127778",
|
|
# "microtimestamp":"1646127777950000",
|
|
# "amount":0.05,
|
|
# "amount_str":"0.05000000",
|
|
# "price":1000,
|
|
# "price_str":"1000.00"
|
|
# },
|
|
# "channel":"private-my_orders_ltcusd-4848701",
|
|
# "event": "order_deleted" # field only present for cancelOrder
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
order = self.safe_value(message, 'data', {})
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
if self.orders is None:
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
stored = self.orders
|
|
subscription = self.safe_value(client.subscriptions, channel)
|
|
symbol = self.safe_string(subscription, 'symbol')
|
|
market = self.market(symbol)
|
|
order['event'] = self.safe_string(message, 'event')
|
|
parsed = self.parse_ws_order(order, market)
|
|
stored.append(parsed)
|
|
client.resolve(self.orders, channel)
|
|
|
|
def parse_ws_order(self, order, market=None):
|
|
#
|
|
# {
|
|
# "id": "1894876776091648",
|
|
# "id_str": "1894876776091648",
|
|
# "order_type": 0,
|
|
# "order_subtype": 0,
|
|
# "datetime": "1751451375",
|
|
# "microtimestamp": "1751451375070000",
|
|
# "amount": 1.1,
|
|
# "amount_str": "1.10000000",
|
|
# "amount_traded": "0",
|
|
# "amount_at_create": "1.10000000",
|
|
# "price": 10.23,
|
|
# "price_str": "10.23",
|
|
# "is_liquidation": False,
|
|
# "trade_account_id": 0
|
|
# }
|
|
#
|
|
id = self.safe_string(order, 'id_str')
|
|
orderTypeRaw = self.safe_string_lower(order, 'order_type')
|
|
side = 'sell' if (orderTypeRaw == '1') else 'buy'
|
|
orderSubTypeRaw = self.safe_string_lower(order, 'order_subtype') # https://www.bitstamp.net/websocket/v2/#:~:text=order_subtype
|
|
orderType: Str = None
|
|
timeInForce: Str = None
|
|
if orderSubTypeRaw == '0':
|
|
orderType = 'limit'
|
|
elif orderSubTypeRaw == '2':
|
|
orderType = 'market'
|
|
elif orderSubTypeRaw == '4':
|
|
orderType = 'limit'
|
|
timeInForce = 'IOC'
|
|
elif orderSubTypeRaw == '6':
|
|
orderType = 'limit'
|
|
timeInForce = 'FOK'
|
|
elif orderSubTypeRaw == '8':
|
|
orderType = 'limit'
|
|
timeInForce = 'GTD'
|
|
price = self.safe_string(order, 'price_str')
|
|
amount = self.safe_string(order, 'amount_str')
|
|
filled = self.safe_string(order, 'amount_traded')
|
|
event = self.safe_string(order, 'event')
|
|
status = None
|
|
if Precise.string_eq(filled, amount):
|
|
status = 'closed'
|
|
elif event == 'order_deleted':
|
|
status = 'canceled'
|
|
timestamp = self.safe_timestamp(order, 'datetime')
|
|
market = self.safe_market(None, market)
|
|
symbol = market['symbol']
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'clientOrderId': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'type': orderType,
|
|
'timeInForce': timeInForce,
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'stopPrice': None,
|
|
'triggerPrice': None,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'average': None,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': None,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
def handle_order_book_subscription(self, client: Client, message):
|
|
channel = self.safe_string(message, 'channel')
|
|
parts = channel.split('_')
|
|
marketId = self.safe_string(parts, 3)
|
|
symbol = self.safe_symbol(marketId)
|
|
self.orderbooks[symbol] = self.order_book()
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "event": "bts:subscription_succeeded",
|
|
# "channel": "detail_order_book_btcusd",
|
|
# "data": {},
|
|
# }
|
|
# {
|
|
# "event": "bts:subscription_succeeded",
|
|
# "channel": "private-my_orders_ltcusd-4848701",
|
|
# "data": {}
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
if channel.find('order_book') > -1:
|
|
self.handle_order_book_subscription(client, message)
|
|
|
|
def handle_subject(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "data": {
|
|
# "timestamp": "1583656800",
|
|
# "microtimestamp": "1583656800237527",
|
|
# "bids": [
|
|
# ["8732.02", "0.00002478", "1207590500704256"],
|
|
# ["8729.62", "0.01600000", "1207590502350849"],
|
|
# ["8727.22", "0.01800000", "1207590504296448"],
|
|
# ],
|
|
# "asks": [
|
|
# ["8735.67", "2.00000000", "1207590693249024"],
|
|
# ["8735.67", "0.01700000", "1207590693634048"],
|
|
# ["8735.68", "1.53294500", "1207590692048896"],
|
|
# ],
|
|
# },
|
|
# "event": "data",
|
|
# "channel": "detail_order_book_btcusd"
|
|
# }
|
|
#
|
|
# private order
|
|
# {
|
|
# "data":{
|
|
# "id":"1463471322288128",
|
|
# "id_str":"1463471322288128",
|
|
# "order_type":1,
|
|
# "datetime":"1646127778",
|
|
# "microtimestamp":"1646127777950000",
|
|
# "amount":0.05,
|
|
# "amount_str":"0.05000000",
|
|
# "price":1000,
|
|
# "price_str":"1000.00"
|
|
# },
|
|
# "channel":"private-my_orders_ltcusd-4848701",
|
|
# "event": "order_deleted" # field only present for cancelOrder
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
methods: dict = {
|
|
'live_trades': self.handle_trade,
|
|
'diff_order_book': self.handle_order_book,
|
|
'private-my_orders': self.handle_orders,
|
|
}
|
|
keys = list(methods.keys())
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
if channel.find(key) > -1:
|
|
method = methods[key]
|
|
method(client, message)
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
# {
|
|
# "event": "bts:error",
|
|
# "channel": '',
|
|
# "data": {code: 4009, message: "Connection is unauthorized."}
|
|
# }
|
|
event = self.safe_string(message, 'event')
|
|
if event == 'bts:error':
|
|
feedback = self.id + ' ' + self.json(message)
|
|
data = self.safe_value(message, 'data', {})
|
|
code = self.safe_number(data, 'code')
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
if not self.handle_error_message(client, message):
|
|
return
|
|
#
|
|
# {
|
|
# "event": "bts:subscription_succeeded",
|
|
# "channel": "detail_order_book_btcusd",
|
|
# "data": {},
|
|
# }
|
|
#
|
|
# {
|
|
# "data": {
|
|
# "timestamp": "1583656800",
|
|
# "microtimestamp": "1583656800237527",
|
|
# "bids": [
|
|
# ["8732.02", "0.00002478", "1207590500704256"],
|
|
# ["8729.62", "0.01600000", "1207590502350849"],
|
|
# ["8727.22", "0.01800000", "1207590504296448"],
|
|
# ],
|
|
# "asks": [
|
|
# ["8735.67", "2.00000000", "1207590693249024"],
|
|
# ["8735.67", "0.01700000", "1207590693634048"],
|
|
# ["8735.68", "1.53294500", "1207590692048896"],
|
|
# ],
|
|
# },
|
|
# "event": "data",
|
|
# "channel": "detail_order_book_btcusd"
|
|
# }
|
|
#
|
|
# {
|
|
# "event": "bts:subscription_succeeded",
|
|
# "channel": "private-my_orders_ltcusd-4848701",
|
|
# "data": {}
|
|
# }
|
|
#
|
|
event = self.safe_string(message, 'event')
|
|
if event == 'bts:subscription_succeeded':
|
|
self.handle_subscription_status(client, message)
|
|
else:
|
|
self.handle_subject(client, message)
|
|
|
|
async def authenticate(self, params={}):
|
|
self.check_required_credentials()
|
|
time = self.milliseconds()
|
|
expiresIn = self.safe_integer(self.options, 'expiresIn')
|
|
if (expiresIn is None) or (time > expiresIn):
|
|
response = await self.privatePostWebsocketsToken(params)
|
|
#
|
|
# {
|
|
# "valid_sec":60,
|
|
# "token":"siPaT4m6VGQCdsDCVbLBemiphHQs552e",
|
|
# "user_id":4848701
|
|
# }
|
|
#
|
|
sessionToken = self.safe_string(response, 'token')
|
|
if sessionToken is not None:
|
|
userId = self.safe_string(response, 'user_id')
|
|
validity = self.safe_integer_product(response, 'valid_sec', 1000)
|
|
self.options['expiresIn'] = self.sum(time, validity)
|
|
self.options['userId'] = userId
|
|
self.options['wsSessionToken'] = sessionToken
|
|
|
|
async def subscribe_private(self, subscription, messageHash, params={}):
|
|
url = self.urls['api']['ws']
|
|
await self.authenticate()
|
|
messageHash += '-' + self.options['userId']
|
|
request: dict = {
|
|
'event': 'bts:subscribe',
|
|
'data': {
|
|
'channel': messageHash,
|
|
'auth': self.options['wsSessionToken'],
|
|
},
|
|
}
|
|
subscription['messageHash'] = messageHash
|
|
return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription)
|