# -*- 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 ` 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 ` """ 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 ` """ 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)