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

1322 lines
58 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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, ArrayCacheByTimestamp
import hashlib
from ccxt.base.types import Any, Balances, Int, Num, Order, OrderBook, OrderSide, OrderType, 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 NotSupported
class hitbtc(ccxt.async_support.hitbtc):
def describe(self) -> Any:
return self.deep_extend(super(hitbtc, self).describe(), {
'has': {
'ws': True,
'watchTicker': True,
'watchTickers': True,
'watchBidsAsks': True,
'watchTrades': True,
'watchTradesForSymbols': False,
'watchOrderBook': True,
'watchBalance': True,
'watchOrders': True,
'watchOHLCV': True,
'watchMyTrades': False,
'createOrderWs': True,
'cancelOrderWs': True,
'fetchOpenOrdersWs': True,
'cancelAllOrdersWs': True,
},
'urls': {
'api': {
'ws': {
'public': 'wss://api.hitbtc.com/api/3/ws/public',
'private': 'wss://api.hitbtc.com/api/3/ws/trading',
},
},
'test': {
'ws': {
'public': 'wss://api.demo.hitbtc.com/api/3/ws/public',
'private': 'wss://api.demo.hitbtc.com/api/3/ws/trading',
},
},
},
'options': {
'tradesLimit': 1000,
'watchTicker': {
'method': 'ticker/{speed}', # 'ticker/{speed}' or 'ticker/price/{speed}'
},
'watchTickers': {
'method': 'ticker/{speed}', # 'ticker/{speed}','ticker/price/{speed}', 'ticker/{speed}/batch', or 'ticker/{speed}/price/batch''
},
'watchBidsAsks': {
'method': 'orderbook/top/{speed}', # 'orderbook/top/{speed}', 'orderbook/top/{speed}/batch'
},
'watchOrderBook': {
'method': 'orderbook/full', # 'orderbook/full', 'orderbook/{depth}/{speed}', 'orderbook/{depth}/{speed}/batch'
},
},
'timeframes': {
'1m': 'M1',
'3m': 'M3',
'5m': 'M5',
'15m': 'M15',
'30m': 'M30',
'1h': 'H1',
'4h': 'H4',
'1d': 'D1',
'1w': 'D7',
'1M': '1M',
},
'streaming': {
'keepAlive': 4000,
},
})
async def authenticate(self):
"""
@ignore
authenticates the user to access private web socket channels
https://api.hitbtc.com/#socket-authentication
:returns dict: response from exchange
"""
self.check_required_credentials()
url = self.urls['api']['ws']['private']
messageHash = 'authenticated'
client = self.client(url)
future = client.reusableFuture(messageHash)
authenticated = self.safe_value(client.subscriptions, messageHash)
if authenticated is None:
timestamp = self.milliseconds()
signature = self.hmac(self.encode(self.number_to_string(timestamp)), self.encode(self.secret), hashlib.sha256, 'hex')
request: dict = {
'method': 'login',
'params': {
'type': 'HS256',
'api_key': self.apiKey,
'timestamp': timestamp,
'signature': signature,
},
}
self.watch(url, messageHash, request, messageHash)
#
# {
# "jsonrpc": "2.0",
# "result": True
# }
#
# # Failure to return results
#
# {
# "jsonrpc": "2.0",
# "error": {
# "code": 1002,
# "message": "Authorization is required or has been failed",
# "description": "invalid signature format"
# }
# }
#
return await future
async def subscribe_public(self, name: str, messageHashPrefix: str, symbols: Strings = None, params={}):
"""
@ignore
:param str name: websocket endpoint name
:param str messageHashPrefix: prefix for the message hash
:param str[] [symbols]: unified CCXT symbol(s)
:param dict [params]: extra parameters specific to the hitbtc api
"""
await self.load_markets()
symbols = self.market_symbols(symbols)
isBatch = name.find('batch') >= 0
url = self.urls['api']['ws']['public']
messageHashes = []
if symbols is not None and not isBatch:
for i in range(0, len(symbols)):
messageHashes.append(messageHashPrefix + '::' + symbols[i])
else:
messageHashes.append(messageHashPrefix)
subscribe: dict = {
'method': 'subscribe',
'id': self.nonce(),
'ch': name,
}
request = self.extend(subscribe, params)
return await self.watch_multiple(url, messageHashes, request, messageHashes)
async def subscribe_private(self, name: str, symbol: Str = None, params={}):
"""
@ignore
:param str name: websocket endpoint name
:param str [symbol]: unified CCXT symbol
:param dict [params]: extra parameters specific to the hitbtc api
"""
await self.load_markets()
await self.authenticate()
url = self.urls['api']['ws']['private']
splitName = name.split('_subscribe')
messageHash = self.safe_string(splitName, 0)
if symbol is not None:
messageHash = messageHash + '::' + symbol
subscribe: dict = {
'method': name,
'params': params,
'id': self.nonce(),
}
return await self.watch(url, messageHash, subscribe, messageHash)
async def trade_request(self, name: str, params={}):
"""
@ignore
:param str name: websocket endpoint name
:param dict [params]: extra parameters specific to the hitbtc api
"""
await self.load_markets()
await self.authenticate()
url = self.urls['api']['ws']['private']
messageHash = str(self.nonce())
subscribe: dict = {
'method': name,
'params': params,
'id': messageHash,
}
return await self.watch(url, messageHash, subscribe, messageHash)
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
https://api.hitbtc.com/#subscribe-to-full-order-book
https://api.hitbtc.com/#subscribe-to-partial-order-book
https://api.hitbtc.com/#subscribe-to-partial-order-book-in-batches
https://api.hitbtc.com/#subscribe-to-top-of-book
https://api.hitbtc.com/#subscribe-to-top-of-book-in-batches
: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
:param str [params.method]: 'orderbook/full', 'orderbook/{depth}/{speed}', 'orderbook/{depth}/{speed}/batch'
:param int [params.depth]: 5 , 10, or 20(default)
:param int [params.speed]: 100(default), 500, or 1000
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
"""
options = self.safe_value(self.options, 'watchOrderBook')
defaultMethod = self.safe_string(options, 'method', 'orderbook/full')
name = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod)
depth = self.safe_string(params, 'depth', '20')
speed = self.safe_string(params, 'depth', '100')
if name == 'orderbook/{depth}/{speed}':
name = 'orderbook/D' + depth + '/' + speed + 'ms'
elif name == 'orderbook/{depth}/{speed}/batch':
name = 'orderbook/D' + depth + '/' + speed + 'ms/batch'
market = self.market(symbol)
request: dict = {
'params': {
'symbols': [market['id']],
},
}
orderbook = await self.subscribe_public(name, 'orderbooks', [symbol], self.deep_extend(request, params))
return orderbook.limit()
def handle_order_book(self, client: Client, message):
#
# {
# "ch": "orderbook/full", # Channel
# "snapshot": {
# "ETHBTC": {
# "t": 1626866578796, # Timestamp in milliseconds
# "s": 27617207, # Sequence number
# "a": [ # Asks
# ["0.060506", "0"],
# ["0.060549", "12.6431"],
# ["0.060570", "0"],
# ["0.060612", "0"]
# ],
# "b": [ # Bids
# ["0.060439", "4.4095"],
# ["0.060414", "0"],
# ["0.060407", "7.3349"],
# ["0.060390", "0"]
# ]
# }
# }
# }
#
snapshot = self.safe_dict(message, 'snapshot')
update = self.safe_dict(message, 'update')
data = snapshot if snapshot else update
type = 'snapshot' if snapshot else 'update'
marketIds = list(data.keys())
for i in range(0, len(marketIds)):
marketId = marketIds[i]
market = self.safe_market(marketId)
symbol = market['symbol']
item = data[marketId]
messageHash = 'orderbooks::' + symbol
if not (symbol in self.orderbooks):
subscription = self.safe_dict(client.subscriptions, messageHash, {})
limit = self.safe_integer(subscription, 'limit')
self.orderbooks[symbol] = self.order_book({}, limit)
orderbook = self.orderbooks[symbol]
timestamp = self.safe_integer(item, 't')
nonce = self.safe_integer(item, 's')
if type == 'snapshot':
parsedSnapshot = self.parse_order_book(item, symbol, timestamp, 'b', 'a')
orderbook.reset(parsedSnapshot)
else:
asks = self.safe_list(item, 'a', [])
bids = self.safe_list(item, 'b', [])
self.handle_deltas(orderbook['asks'], asks)
self.handle_deltas(orderbook['bids'], bids)
orderbook['timestamp'] = timestamp
orderbook['datetime'] = self.iso8601(timestamp)
orderbook['nonce'] = nonce
orderbook['symbol'] = symbol
self.orderbooks[symbol] = orderbook
client.resolve(orderbook, messageHash)
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])
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
https://api.hitbtc.com/#subscribe-to-ticker
https://api.hitbtc.com/#subscribe-to-ticker-in-batches
https://api.hitbtc.com/#subscribe-to-mini-ticker
https://api.hitbtc.com/#subscribe-to-mini-ticker-in-batches
:param str symbol: unified symbol of the market to fetch the ticker for
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.method]: 'ticker/{speed}'(default), or 'ticker/price/{speed}'
:param str [params.speed]: '1s'(default), or '3s'
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
"""
ticker = await self.watch_tickers([symbol], params)
return self.safe_value(ticker, symbol)
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 a specific market
:param str[] [symbols]:
:param dict params: extra parameters specific to the exchange API endpoint
:param str params['method']: 'ticker/{speed}' ,'ticker/price/{speed}', 'ticker/{speed}/batch'(default), or 'ticker/{speed}/price/batch''
:param str params['speed']: '1s'(default), or '3s'
:returns dict: a `ticker structure <https://docs.ccxt.com/en/latest/manual.html#ticker-structure>`
"""
await self.load_markets()
symbols = self.market_symbols(symbols)
options = self.safe_value(self.options, 'watchTicker')
defaultMethod = self.safe_string(options, 'method', 'ticker/{speed}/batch')
method = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod)
speed = self.safe_string(params, 'speed', '1s')
name = self.implode_params(method, {'speed': speed})
params = self.omit(params, ['method', 'speed'])
marketIds = []
if symbols is None:
marketIds.append('*')
else:
for i in range(0, len(symbols)):
marketId = self.market_id(symbols[i])
marketIds.append(marketId)
request: dict = {
'params': {
'symbols': marketIds,
},
}
newTickers = await self.subscribe_public(name, 'tickers', symbols, self.deep_extend(request, params))
if self.newUpdates:
if not isinstance(newTickers, list):
tickers: dict = {}
tickers[newTickers['symbol']] = newTickers
return tickers
return self.filter_by_array(newTickers, 'symbol', symbols)
def handle_ticker(self, client: Client, message):
#
# {
# "ch": "ticker/1s",
# "data": {
# "ETHBTC": {
# "t": 1614815872000, # Timestamp in milliseconds
# "a": "0.031175", # Best ask
# "A": "0.03329", # Best ask quantity
# "b": "0.031148", # Best bid
# "B": "0.10565", # Best bid quantity
# "c": "0.031210", # Last price
# "o": "0.030781", # Open price
# "h": "0.031788", # High price
# "l": "0.030733", # Low price
# "v": "62.587", # Base asset volume
# "q": "1.951420577", # Quote asset volume
# "p": "0.000429", # Price change
# "P": "1.39", # Price change percent
# "L": 1182694927 # Last trade identifier
# }
# }
# }
#
# {
# "ch": "ticker/price/1s",
# "data": {
# "BTCUSDT": {
# "t": 1614815872030,
# "o": "32636.79",
# "c": "32085.51",
# "h": "33379.92",
# "l": "30683.28",
# "v": "11.90667",
# "q": "384081.1955629"
# }
# }
# }
#
data = self.safe_value(message, 'data', {})
marketIds = list(data.keys())
result = []
topic = 'tickers'
for i in range(0, len(marketIds)):
marketId = marketIds[i]
market = self.safe_market(marketId)
symbol = market['symbol']
ticker = self.parse_ws_ticker(data[marketId], market)
self.tickers[symbol] = ticker
result.append(ticker)
messageHash = topic + '::' + symbol
client.resolve(ticker, messageHash)
client.resolve(result, topic)
def parse_ws_ticker(self, ticker, market=None):
#
# {
# "t": 1614815872000, # Timestamp in milliseconds
# "a": "0.031175", # Best ask
# "A": "0.03329", # Best ask quantity
# "b": "0.031148", # Best bid
# "B": "0.10565", # Best bid quantity
# "c": "0.031210", # Last price
# "o": "0.030781", # Open price
# "h": "0.031788", # High price
# "l": "0.030733", # Low price
# "v": "62.587", # Base asset volume
# "q": "1.951420577", # Quote asset volume
# "p": "0.000429", # Price change
# "P": "1.39", # Price change percent
# "L": 1182694927 # Last trade identifier
# }
#
# {
# "t": 1614815872030,
# "o": "32636.79",
# "c": "32085.51",
# "h": "33379.92",
# "l": "30683.28",
# "v": "11.90667",
# "q": "384081.1955629"
# }
#
timestamp = self.safe_integer(ticker, 't')
symbol = self.safe_symbol(None, market)
last = self.safe_string(ticker, 'c')
return self.safe_ticker({
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'high': self.safe_string(ticker, 'h'),
'low': self.safe_string(ticker, 'l'),
'bid': self.safe_string(ticker, 'b'),
'bidVolume': self.safe_string(ticker, 'B'),
'ask': self.safe_string(ticker, 'a'),
'askVolume': self.safe_string(ticker, 'A'),
'vwap': None,
'open': self.safe_string(ticker, 'o'),
'close': last,
'last': last,
'previousClose': None,
'change': None,
'percentage': None,
'average': None,
'baseVolume': self.safe_string(ticker, 'v'),
'quoteVolume': self.safe_string(ticker, 'q'),
'info': ticker,
}, market)
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
"""
watches best bid & ask for symbols
https://api.hitbtc.com/#subscribe-to-top-of-book
: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.method]: 'orderbook/top/{speed}' or 'orderbook/top/{speed}/batch(default)'
:param str [params.speed]: '100ms'(default) or '500ms' or '1000ms'
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
"""
await self.load_markets()
symbols = self.market_symbols(symbols, None, False)
options = self.safe_value(self.options, 'watchBidsAsks')
defaultMethod = self.safe_string(options, 'method', 'orderbook/top/{speed}/batch')
method = self.safe_string_2(params, 'method', 'defaultMethod', defaultMethod)
speed = self.safe_string(params, 'speed', '100ms')
name = self.implode_params(method, {'speed': speed})
params = self.omit(params, ['method', 'speed'])
marketIds = self.market_ids(symbols)
request: dict = {
'params': {
'symbols': marketIds,
},
}
newTickers = await self.subscribe_public(name, 'bidask', symbols, self.deep_extend(request, params))
if self.newUpdates:
if not isinstance(newTickers, list):
tickers: dict = {}
tickers[newTickers['symbol']] = newTickers
return tickers
return self.filter_by_array(newTickers, 'symbol', symbols)
def handle_bid_ask(self, client: Client, message):
#
# {
# "ch": "orderbook/top/100ms", # or 'orderbook/top/100ms/batch'
# "data": {
# "BTCUSDT": {
# "t": 1727276919771,
# "a": "63931.45",
# "A": "0.02879",
# "b": "63926.97",
# "B": "0.00100"
# }
# }
# }
#
data = self.safe_dict(message, 'data', {})
marketIds = list(data.keys())
result = []
topic = 'bidask'
for i in range(0, len(marketIds)):
marketId = marketIds[i]
market = self.safe_market(marketId)
symbol = market['symbol']
ticker = self.parse_ws_bid_ask(data[marketId], market)
self.bidsasks[symbol] = ticker
result.append(ticker)
messageHash = topic + '::' + symbol
client.resolve(ticker, messageHash)
client.resolve(result, topic)
def parse_ws_bid_ask(self, ticker, market=None):
timestamp = self.safe_integer(ticker, 't')
return self.safe_ticker({
'symbol': market['symbol'],
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'ask': self.safe_string(ticker, 'a'),
'askVolume': self.safe_string(ticker, 'A'),
'bid': self.safe_string(ticker, 'b'),
'bidVolume': self.safe_string(ticker, 'B'),
'info': ticker,
}, market)
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
https://api.hitbtc.com/#subscribe-to-trades
: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)
request: dict = {
'params': {
'symbols': [market['id']],
},
}
if limit is not None:
request['limit'] = limit
name = 'trades'
trades = await self.subscribe_public(name, 'trades', [symbol], self.deep_extend(request, params))
if self.newUpdates:
limit = trades.getLimit(symbol, limit)
return self.filter_by_since_limit(trades, since, limit, 'timestamp')
def handle_trades(self, client: Client, message):
#
# {
# "result": {
# "ch": "trades", # Channel
# "subscriptions": ["ETHBTC", "BTCUSDT"]
# },
# "id": 123
# }
#
# Notification snapshot
#
# {
# "ch": "trades", # Channel
# "snapshot": {
# "BTCUSDT": [{
# "t": 1626861109494, # Timestamp in milliseconds
# "i": 1555634969, # Trade identifier
# "p": "30881.96", # Price
# "q": "12.66828", # Quantity
# "s": "buy" # Side
# }]
# }
# }
#
# Notification update
#
# {
# "ch": "trades",
# "update": {
# "BTCUSDT": [{
# "t": 1626861123552,
# "i": 1555634969,
# "p": "30877.68",
# "q": "0.00006",
# "s": "sell"
# }]
# }
# }
#
data = self.safe_value_2(message, 'snapshot', 'update', {})
marketIds = list(data.keys())
for i in range(0, len(marketIds)):
marketId = marketIds[i]
market = self.safe_market(marketId)
tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
symbol = market['symbol']
stored = self.safe_value(self.trades, symbol)
if stored is None:
stored = ArrayCache(tradesLimit)
self.trades[symbol] = stored
trades = self.parse_ws_trades(data[marketId], market)
for j in range(0, len(trades)):
stored.append(trades[j])
messageHash = 'trades::' + symbol
client.resolve(stored, messageHash)
return message
def parse_ws_trades(self, trades, market: object = None, since: Int = None, limit: Int = None, params={}):
trades = self.to_array(trades)
result = []
for i in range(0, len(trades)):
trade = self.extend(self.parse_ws_trade(trades[i], market), params)
result.append(trade)
result = self.sort_by_2(result, 'timestamp', 'id')
symbol = self.safe_string(market, 'symbol')
return self.filter_by_symbol_since_limit(result, symbol, since, limit)
def parse_ws_trade(self, trade, market=None):
#
# {
# "t": 1626861123552, # Timestamp in milliseconds
# "i": 1555634969, # Trade identifier
# "p": "30877.68", # Price
# "q": "0.00006", # Quantity
# "s": "sell" # Side
# }
#
timestamp = self.safe_integer(trade, 't')
return self.safe_trade({
'info': trade,
'id': self.safe_string(trade, 'i'),
'order': None,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'symbol': self.safe_string(market, 'symbol'),
'type': None,
'side': self.safe_string(trade, 's'),
'takerOrMaker': None,
'price': self.safe_string(trade, 'p'),
'amount': self.safe_string(trade, 'q'),
'cost': None,
'fee': None,
}, market)
async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
"""
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
https://api.hitbtc.com/#subscribe-to-candles
: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]: not used by hitbtc watchOHLCV
:param int [limit]: 0 1000, default value = 0(no history returned)
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns int[][]: A list of candles ordered, open, high, low, close, volume
"""
period = self.safe_string(self.timeframes, timeframe, timeframe)
name = 'candles/' + period
market = self.market(symbol)
request: dict = {
'params': {
'symbols': [market['id']],
},
}
if limit is not None:
request['params']['limit'] = limit
ohlcv = await self.subscribe_public(name, 'candles', [symbol], self.deep_extend(request, params))
if self.newUpdates:
limit = ohlcv.getLimit(symbol, limit)
return self.filter_by_since_limit(ohlcv, since, limit, 0)
def handle_ohlcv(self, client: Client, message):
#
# {
# "ch": "candles/M1", # Channel
# "snapshot": {
# "BTCUSDT": [{
# "t": 1626860340000, # Message timestamp
# "o": "30881.95", # Open price
# "c": "30890.96", # Last price
# "h": "30900.8", # High price
# "l": "30861.27", # Low price
# "v": "1.27852", # Base asset volume
# "q": "39493.9021811" # Quote asset volume
# }
# ...
# ]
# }
# }
#
# {
# "ch": "candles/M1",
# "update": {
# "ETHBTC": [{
# "t": 1626860880000,
# "o": "0.060711",
# "c": "0.060749",
# "h": "0.060749",
# "l": "0.060711",
# "v": "12.2800",
# "q": "0.7455339675"
# }]
# }
# }
#
data = self.safe_value_2(message, 'snapshot', 'update', {})
marketIds = list(data.keys())
channel = self.safe_string(message, 'ch')
splitChannel = channel.split('/')
period = self.safe_string(splitChannel, 1)
timeframe = self.find_timeframe(period)
for i in range(0, len(marketIds)):
marketId = marketIds[i]
market = self.safe_market(marketId)
symbol = market['symbol']
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)
self.ohlcvs[symbol][timeframe] = stored
ohlcvs = self.parse_ws_ohlcvs(data[marketId], market)
for j in range(0, len(ohlcvs)):
stored.append(ohlcvs[j])
messageHash = 'candles::' + symbol
client.resolve(stored, messageHash)
return message
def parse_ws_ohlcv(self, ohlcv, market=None) -> list:
#
# {
# "t": 1626860340000, # Message timestamp
# "o": "30881.95", # Open price
# "c": "30890.96", # Last price
# "h": "30900.8", # High price
# "l": "30861.27", # Low price
# "v": "1.27852", # Base asset volume
# "q": "39493.9021811" # Quote asset volume
# }
#
return [
self.safe_integer(ohlcv, 't'),
self.safe_number(ohlcv, 'o'),
self.safe_number(ohlcv, 'h'),
self.safe_number(ohlcv, 'l'),
self.safe_number(ohlcv, 'c'),
self.safe_number(ohlcv, 'v'),
]
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
https://api.hitbtc.com/#subscribe-to-reports
https://api.hitbtc.com/#subscribe-to-reports-2
https://api.hitbtc.com/#subscribe-to-reports-3
:param str [symbol]: unified CCXT market symbol
:param int [since]: timestamp in ms of the earliest order to fetch
:param int [limit]: the maximum amount of orders to fetch
:param dict [params]: extra parameters specific to the exchange API endpoint
:returns dict[]: a list of `order structures <https://docs.ccxt.com/en/latest/manual.html#order-structure>`
"""
await self.load_markets()
marketType = None
market = None
if symbol is not None:
market = self.market(symbol)
marketType, params = self.handle_market_type_and_params('watchOrders', market, params)
name = self.get_supported_mapping(marketType, {
'spot': 'spot_subscribe',
'margin': 'margin_subscribe',
'swap': 'futures_subscribe',
'future': 'futures_subscribe',
})
orders = await self.subscribe_private(name, symbol, params)
if self.newUpdates:
limit = orders.getLimit(symbol, limit)
return self.filter_by_since_limit(orders, since, limit, 'timestamp')
def handle_order(self, client: Client, message):
#
# {
# "jsonrpc": "2.0",
# "method": "spot_order", # "margin_order", "future_order"
# "params": {
# "id": 584244931496,
# "client_order_id": "b5acd79c0a854b01b558665bcf379456",
# "symbol": "BTCUSDT",
# "side": "buy",
# "status": "new",
# "type": "limit",
# "time_in_force": "GTC",
# "quantity": "0.01000",
# "quantity_cumulative": "0",
# "price": "0.01", # only updates and snapshots
# "post_only": False,
# "reduce_only": False, # only margin and contract
# "display_quantity": "0", # only updates and snapshot
# "created_at": "2021-07-02T22:52:32.864Z",
# "updated_at": "2021-07-02T22:52:32.864Z",
# "trade_id": 1361977606, # only trades
# "trade_quantity": "0.00001", # only trades
# "trade_price": "49595.04", # only trades
# "trade_fee": "0.001239876000", # only trades
# "trade_taker": True, # only trades, only spot
# "trade_position_id": 485308, # only trades, only margin
# "report_type": "new" # "trade", "status"(snapshot)
# }
# }
#
# {
# "jsonrpc": "2.0",
# "method": "spot_orders", # "margin_orders", "future_orders"
# "params": [
# {
# "id": 584244931496,
# "client_order_id": "b5acd79c0a854b01b558665bcf379456",
# "symbol": "BTCUSDT",
# "side": "buy",
# "status": "new",
# "type": "limit",
# "time_in_force": "GTC",
# "quantity": "0.01000",
# "quantity_cumulative": "0",
# "price": "0.01", # only updates and snapshots
# "post_only": False,
# "reduce_only": False, # only margin and contract
# "display_quantity": "0", # only updates and snapshot
# "created_at": "2021-07-02T22:52:32.864Z",
# "updated_at": "2021-07-02T22:52:32.864Z",
# "trade_id": 1361977606, # only trades
# "trade_quantity": "0.00001", # only trades
# "trade_price": "49595.04", # only trades
# "trade_fee": "0.001239876000", # only trades
# "trade_taker": True, # only trades, only spot
# "trade_position_id": 485308, # only trades, only margin
# "report_type": "new" # "trade", "status"(snapshot)
# }
# ]
# }
#
if self.orders is None:
limit = self.safe_integer(self.options, 'ordersLimit')
self.orders = ArrayCacheBySymbolById(limit)
data = self.safe_value(message, 'params', [])
if isinstance(data, list):
for i in range(0, len(data)):
order = data[i]
self.handle_order_helper(client, message, order)
else:
self.handle_order_helper(client, message, data)
return message
def handle_order_helper(self, client: Client, message, order):
orders = self.orders
marketId = self.safe_string_lower_2(order, 'instrument', 'symbol')
method = self.safe_string(message, 'method')
splitMethod = method.split('_order')
messageHash = self.safe_string(splitMethod, 0)
symbol = self.safe_symbol(marketId)
parsed = self.parse_order(order)
orders.append(parsed)
client.resolve(orders, messageHash)
client.resolve(orders, messageHash + '::' + symbol)
def parse_ws_order_trade(self, trade, market=None):
#
# {
# "id": 584244931496,
# "client_order_id": "b5acd79c0a854b01b558665bcf379456",
# "symbol": "BTCUSDT",
# "side": "buy",
# "status": "new",
# "type": "limit",
# "time_in_force": "GTC",
# "quantity": "0.01000",
# "quantity_cumulative": "0",
# "price": "0.01", # only updates and snapshots
# "post_only": False,
# "reduce_only": False, # only margin and contract
# "display_quantity": "0", # only updates and snapshot
# "created_at": "2021-07-02T22:52:32.864Z",
# "updated_at": "2021-07-02T22:52:32.864Z",
# "trade_id": 1361977606, # only trades
# "trade_quantity": "0.00001", # only trades
# "trade_price": "49595.04", # only trades
# "trade_fee": "0.001239876000", # only trades
# "trade_taker": True, # only trades, only spot
# "trade_position_id": 485308, # only trades, only margin
# "report_type": "new" # "trade", "status"(snapshot)
# }
#
timestamp = self.safe_integer(trade, 'created_at')
marketId = self.safe_string(trade, 'symbol')
return self.safe_trade({
'info': trade,
'id': self.safe_string(trade, 'trade_id'),
'order': self.safe_string(trade, 'id'),
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'symbol': self.safe_market(marketId, market),
'type': None,
'side': self.safe_string(trade, 'side'),
'takerOrMaker': self.safe_string(trade, 'trade_taker'),
'price': self.safe_string(trade, 'trade_price'),
'amount': self.safe_string(trade, 'trade_quantity'),
'cost': None,
'fee': {
'cost': self.safe_string(trade, 'trade_fee'),
'currency': None,
'rate': None,
},
}, market)
def parse_ws_order(self, order, market=None):
#
# {
# "id": 584244931496,
# "client_order_id": "b5acd79c0a854b01b558665bcf379456",
# "symbol": "BTCUSDT",
# "side": "buy",
# "status": "new",
# "type": "limit",
# "time_in_force": "GTC",
# "quantity": "0.01000",
# "quantity_cumulative": "0",
# "price": "0.01", # only updates and snapshots
# "post_only": False,
# "reduce_only": False, # only margin and contract
# "display_quantity": "0", # only updates and snapshot
# "created_at": "2021-07-02T22:52:32.864Z",
# "updated_at": "2021-07-02T22:52:32.864Z",
# "trade_id": 1361977606, # only trades
# "trade_quantity": "0.00001", # only trades
# "trade_price": "49595.04", # only trades
# "trade_fee": "0.001239876000", # only trades
# "trade_taker": True, # only trades, only spot
# "trade_position_id": 485308, # only trades, only margin
# "report_type": "new" # "trade", "status"(snapshot)
# }
#
timestamp = self.safe_string(order, 'created_at')
marketId = self.safe_string(order, 'symbol')
market = self.safe_market(marketId, market)
tradeId = self.safe_string(order, 'trade_id')
trades = None
if tradeId is not None:
trade = self.parse_ws_order_trade(order, market)
trades = [trade]
rawStatus = self.safe_string(order, 'status')
report_type = self.safe_string(order, 'report_type')
parsedStatus = None
if report_type == 'canceled':
parsedStatus = self.parse_order_status(report_type)
else:
parsedStatus = self.parse_order_status(rawStatus)
return self.safe_order({
'info': order,
'id': self.safe_string(order, 'id'),
'clientOrderId': self.safe_string(order, 'client_order_id'),
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'lastTradeTimestamp': None,
'symbol': market['symbol'],
'price': self.safe_string(order, 'price'),
'amount': self.safe_string(order, 'quantity'),
'type': self.safe_string(order, 'type'),
'side': self.safe_string_upper(order, 'side'),
'timeInForce': self.safe_string(order, 'time_in_force'),
'postOnly': self.safe_string(order, 'post_only'),
'reduceOnly': self.safe_value(order, 'reduce_only'),
'filled': None,
'remaining': None,
'cost': None,
'status': parsedStatus,
'average': None,
'trades': trades,
'fee': None,
}, market)
async def watch_balance(self, params={}) -> Balances:
"""
watches balance updates, cannot subscribe to margin account balances
https://api.hitbtc.com/#subscribe-to-spot-balances
https://api.hitbtc.com/#subscribe-to-futures-balances
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.type]: 'spot', 'swap', or 'future'
EXCHANGE SPECIFIC PARAMETERS
:param str [params.mode]: 'updates' or 'batches'(default), 'updates' = messages arrive after balance updates, 'batches' = messages arrive at equal intervals if there were any updates
:returns dict[]: a list of `balance structures <https://docs.ccxt.com/#/?id=balance-structure>`
"""
await self.load_markets()
type = None
type, params = self.handle_market_type_and_params('watchBalance', None, params)
name = self.get_supported_mapping(type, {
'spot': 'spot_balance_subscribe',
'swap': 'futures_balance_subscribe',
'future': 'futures_balance_subscribe',
})
mode = self.safe_string(params, 'mode', 'batches')
params = self.omit(params, 'mode')
request: dict = {
'mode': mode,
}
return await self.subscribe_private(name, None, self.extend(request, params))
async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
"""
create a trade order
https://api.hitbtc.com/#create-new-spot-order
https://api.hitbtc.com/#create-margin-order
https://api.hitbtc.com/#create-futures-order
:param str symbol: unified symbol of the market to create an order in
:param str type: 'market' or 'limit'
:param str side: 'buy' or 'sell'
:param float amount: how much of currency you want to trade in units of base currency
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported for spot-margin, swap supports both, default is 'cross'
:param bool [params.margin]: True for creating a margin order
:param float [params.triggerPrice]: The price at which a trigger order is triggered at
:param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately
:param str [params.timeInForce]: "GTC", "IOC", "FOK", "Day", "GTD"
:returns dict: an `order structure <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
"""
await self.load_markets()
market = self.market(symbol)
request = None
marketType = None
marketType, params = self.handle_market_type_and_params('createOrder', market, params)
marginMode = None
marginMode, params = self.handle_margin_mode_and_params('createOrder', params)
request, params = self.create_order_request(market, marketType, type, side, amount, price, marginMode, params)
request = self.extend(request, params)
if marketType == 'swap':
return await self.trade_request('futures_new_order', request)
elif (marketType == 'margin') or (marginMode is not None):
return await self.trade_request('margin_new_order', request)
else:
return await self.trade_request('spot_new_order', request)
async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order:
"""
https://api.hitbtc.com/#cancel-spot-order-2
https://api.hitbtc.com/#cancel-futures-order-2
https://api.hitbtc.com/#cancel-margin-order-2
cancels an open order
:param str id: order id
:param str symbol: unified symbol of the market the order was made in
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported
:param bool [params.margin]: True for canceling a margin order
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
"""
await self.load_markets()
market = None
request = {
'client_order_id': id,
}
if symbol is not None:
market = self.market(symbol)
marketType = None
marketType, params = self.handle_market_type_and_params('cancelOrderWs', market, params)
marginMode, query = self.handle_margin_mode_and_params('cancelOrderWs', params)
request = self.extend(request, query)
if marketType == 'swap':
return await self.trade_request('futures_cancel_order', request)
elif (marketType == 'margin') or (marginMode is not None):
return await self.trade_request('margin_cancel_order', request)
else:
return await self.trade_request('spot_cancel_order', request)
async def cancel_all_orders_ws(self, symbol: Str = None, params={}) -> List[Order]:
"""
https://api.hitbtc.com/#cancel-spot-orders
https://api.hitbtc.com/#cancel-futures-order-3
cancel all open orders
:param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported
:param bool [params.margin]: True for canceling margin orders
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
"""
await self.load_markets()
market = None
if symbol is not None:
market = self.market(symbol)
marketType = None
marketType, params = self.handle_market_type_and_params('cancelAllOrdersWs', market, params)
marginMode = None
marginMode, params = self.handle_margin_mode_and_params('cancelAllOrdersWs', params)
if marketType == 'swap':
return await self.trade_request('futures_cancel_orders', params)
elif (marketType == 'margin') or (marginMode is not None):
raise NotSupported(self.id + ' cancelAllOrdersWs is not supported for margin orders')
else:
return await self.trade_request('spot_cancel_orders', params)
async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
"""
https://api.hitbtc.com/#get-active-futures-orders-2
https://api.hitbtc.com/#get-margin-orders
https://api.hitbtc.com/#get-active-spot-orders
fetch all unfilled currently open orders
:param str symbol: unified market symbol
:param int [since]: the earliest time in ms to fetch open orders for
:param int [limit]: the maximum number of open orders structures to retrieve
:param dict [params]: extra parameters specific to the exchange API endpoint
:param str [params.marginMode]: 'cross' or 'isolated' only 'isolated' is supported
:param bool [params.margin]: True for fetching open margin orders
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
"""
await self.load_markets()
market = None
request: dict = {}
if symbol is not None:
market = self.market(symbol)
request['symbol'] = market['id']
marketType = None
marketType, params = self.handle_market_type_and_params('fetchOpenOrdersWs', market, params)
marginMode = None
marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrdersWs', params)
if marketType == 'swap':
return await self.trade_request('futures_get_orders', request)
elif (marketType == 'margin') or (marginMode is not None):
return await self.trade_request('margin_get_orders', request)
else:
return await self.trade_request('spot_get_orders', request)
def handle_balance(self, client: Client, message):
#
# {
# "jsonrpc": "2.0",
# "method": "futures_balance",
# "params": [
# {
# "currency": "BCN",
# "available": "100.000000000000",
# "reserved": "0",
# "reserved_margin": "0"
# },
# ...
# ]
# }
#
messageHash = self.safe_string(message, 'method')
params = self.safe_value(message, 'params')
balance = self.parse_balance(params)
self.balance = self.deep_extend(self.balance, balance)
client.resolve(self.balance, messageHash)
def handle_notification(self, client: Client, message):
#
# {jsonrpc: "2.0", result: True, id: null}
#
return message
def handle_order_request(self, client: Client, message):
#
# createOrderWs, cancelOrderWs
#
# {
# "jsonrpc": "2.0",
# "result": {
# "id": 1130310696965,
# "client_order_id": "OPC2oyHSkEBqIpPtniLqeW-597hUL3Yo",
# "symbol": "ADAUSDT",
# "side": "buy",
# "status": "new",
# "type": "limit",
# "time_in_force": "GTC",
# "quantity": "4",
# "quantity_cumulative": "0",
# "price": "0.3300000",
# "post_only": False,
# "created_at": "2023-11-17T14:58:15.903Z",
# "updated_at": "2023-11-17T14:58:15.903Z",
# "original_client_order_id": "d6b645556af740b1bd1683400fd9cbce", # spot_replace_order only
# "report_type": "new"
# "margin_mode": "isolated", # margin and future only
# "reduce_only": False, # margin and future only
# },
# "id": 1700233093414
# }
#
messageHash = self.safe_string(message, 'id')
result = self.safe_value(message, 'result', {})
if isinstance(result, list):
parsedOrders = []
for i in range(0, len(result)):
parsedOrder = self.parse_ws_order(result[i])
parsedOrders.append(parsedOrder)
client.resolve(parsedOrders, messageHash)
else:
parsedOrder = self.parse_ws_order(result)
client.resolve(parsedOrder, messageHash)
return message
def handle_message(self, client: Client, message):
if self.handle_error(client, message):
return
channel = self.safe_string_2(message, 'ch', 'method')
if channel is not None:
splitChannel = channel.split('/')
channel = self.safe_string(splitChannel, 0)
if channel == 'orderbook':
channel2 = self.safe_string(splitChannel, 1)
if channel2 is not None and channel2 == 'top':
channel = 'orderbook/top'
methods: dict = {
'candles': self.handle_ohlcv,
'ticker': self.handle_ticker,
'trades': self.handle_trades,
'orderbook': self.handle_order_book,
'orderbook/top': self.handle_bid_ask,
'spot_order': self.handle_order,
'spot_orders': self.handle_order,
'margin_order': self.handle_order,
'margin_orders': self.handle_order,
'futures_order': self.handle_order,
'futures_orders': self.handle_order,
'spot_balance': self.handle_balance,
'futures_balance': self.handle_balance,
}
method = self.safe_value(methods, channel)
if method is not None:
method(client, message)
else:
result = self.safe_value(message, 'result')
clientOrderId = self.safe_string(result, 'client_order_id')
if clientOrderId is not None:
self.handle_order_request(client, message)
if (result is True) and not ('id' in message):
self.handle_authenticate(client, message)
if isinstance(result, list):
# to do improve self, not very reliable right now
first = self.safe_value(result, 0, {})
arrayLength = len(result)
if (arrayLength == 0) or ('client_order_id' in first):
self.handle_order_request(client, message)
def handle_authenticate(self, client: Client, message):
#
# {
# "jsonrpc": "2.0",
# "result": True
# }
#
success = self.safe_value(message, 'result')
messageHash = 'authenticated'
if success:
future = self.safe_value(client.futures, messageHash)
future.resolve(True)
else:
error = AuthenticationError(self.id + ' ' + self.json(message))
client.reject(error, messageHash)
if messageHash in client.subscriptions:
del client.subscriptions[messageHash]
return message
def handle_error(self, client: Client, message):
#
# {
# jsonrpc: '2.0',
# error: {
# code: 20001,
# message: 'Insufficient funds',
# description: 'Check that the funds are sufficient, given commissions'
# },
# id: 1700228604325
# }
#
error = self.safe_value(message, 'error')
if error is not None:
try:
code = self.safe_value(error, 'code')
errorMessage = self.safe_string(error, 'message')
description = self.safe_string(error, 'description')
feedback = self.id + ' ' + description
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback)
raise ExchangeError(feedback) # unknown message
except Exception as e:
if isinstance(e, AuthenticationError):
messageHash = 'authenticated'
client.reject(e, messageHash)
if messageHash in client.subscriptions:
del client.subscriptions[messageHash]
else:
id = self.safe_string(message, 'id')
client.reject(e, id)
return True
return None