2713 lines
116 KiB
Python
2713 lines
116 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, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Position, 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 RateLimitExceeded
|
|
from ccxt.base.errors import ChecksumError
|
|
from ccxt.base.errors import UnsubscribeError
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class bitget(ccxt.async_support.bitget):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bitget, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'createOrderWs': False,
|
|
'editOrderWs': False,
|
|
'fetchOpenOrdersWs': False,
|
|
'fetchOrderWs': False,
|
|
'cancelOrderWs': False,
|
|
'cancelOrdersWs': False,
|
|
'cancelAllOrdersWs': False,
|
|
'watchBalance': True,
|
|
'watchMyTrades': True,
|
|
'watchOHLCV': True,
|
|
'watchOHLCVForSymbols': False,
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchOrders': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchBidsAsks': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'watchPositions': True,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': {
|
|
'public': 'wss://ws.bitget.com/v2/ws/public',
|
|
'private': 'wss://ws.bitget.com/v2/ws/private',
|
|
'utaPublic': 'wss://ws.bitget.com/v3/ws/public',
|
|
'utaPrivate': 'wss://ws.bitget.com/v3/ws/private',
|
|
},
|
|
'demo': {
|
|
'public': 'wss://wspap.bitget.com/v2/ws/public',
|
|
'private': 'wss://wspap.bitget.com/v2/ws/private',
|
|
'utaPublic': 'wss://wspap.bitget.com/v3/ws/public',
|
|
'utaPrivate': 'wss://wspap.bitget.com/v3/ws/private',
|
|
},
|
|
},
|
|
},
|
|
'options': {
|
|
'tradesLimit': 1000,
|
|
'OHLCVLimit': 1000,
|
|
# WS timeframes differ from REST timeframes
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'3m': '3m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'1h': '1H',
|
|
'4h': '4H',
|
|
'6h': '6H',
|
|
'12h': '12H',
|
|
'1d': '1D',
|
|
'1w': '1W',
|
|
},
|
|
'watchOrderBook': {
|
|
'checksum': True,
|
|
},
|
|
'watchTrades': {
|
|
'ignoreDuplicates': True,
|
|
},
|
|
},
|
|
'streaming': {
|
|
'ping': self.ping,
|
|
},
|
|
'exceptions': {
|
|
'ws': {
|
|
'exact': {
|
|
'30001': BadRequest, # {"event":"error","code":30001,"msg":"instType:sp,channel:candleNone,instId:BTCUSDT doesn't exist"}
|
|
'30002': AuthenticationError, # illegal request
|
|
'30003': BadRequest, # invalid op
|
|
'30004': AuthenticationError, # requires login
|
|
'30005': AuthenticationError, # login failed
|
|
'30006': RateLimitExceeded, # too many requests
|
|
'30007': RateLimitExceeded, # request over limit,connection close
|
|
'30011': AuthenticationError, # invalid ACCESS_KEY
|
|
'30012': AuthenticationError, # invalid ACCESS_PASSPHRASE
|
|
'30013': AuthenticationError, # invalid ACCESS_TIMESTAMP
|
|
'30014': BadRequest, # Request timestamp expired
|
|
'30015': AuthenticationError, # {event: 'error', code: 30015, msg: 'Invalid sign'}
|
|
'30016': BadRequest, # {event: 'error', code: 30016, msg: 'Param error'}
|
|
},
|
|
'broad': {},
|
|
},
|
|
},
|
|
})
|
|
|
|
def get_inst_type(self, market, uta: bool = False, params={}):
|
|
if (uta is None) or not uta:
|
|
uta, params = self.handle_option_and_params(params, 'getInstType', 'uta', False)
|
|
instType = None
|
|
if market is None:
|
|
instType, params = self.handleProductTypeAndParams(None, params)
|
|
elif (market['swap']) or (market['future']):
|
|
instType, params = self.handleProductTypeAndParams(market, params)
|
|
else:
|
|
instType = 'SPOT'
|
|
instypeAux = None
|
|
instypeAux, params = self.handle_option_and_params(params, 'getInstType', 'instType', instType)
|
|
instType = instypeAux
|
|
if uta:
|
|
instType = instType.lower()
|
|
return [instType, params]
|
|
|
|
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://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel
|
|
|
|
:param str symbol: unified symbol of the market to watch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash = 'ticker:' + symbol
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchTicker', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
symbolOrInstId = 'symbol' if uta else 'instId'
|
|
args[topicOrChannel] = 'ticker'
|
|
args[symbolOrInstId] = market['id']
|
|
return await self.watch_public(messageHash, args, params)
|
|
|
|
async def un_watch_ticker(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unsubscribe from the ticker channel
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel
|
|
|
|
:param str symbol: unified symbol of the market to unwatch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns any: status of the unwatch request
|
|
"""
|
|
await self.load_markets()
|
|
return await self.un_watch_channel(symbol, 'ticker', 'ticker', 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
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel
|
|
|
|
:param str[] symbols: unified symbol of the market to watch the tickers for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
market = self.market(symbols[0])
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchTickers', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
topics = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
marketInner = self.market(symbol)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
symbolOrInstId = 'symbol' if uta else 'instId'
|
|
args[topicOrChannel] = 'ticker'
|
|
args[symbolOrInstId] = marketInner['id']
|
|
topics.append(args)
|
|
messageHashes.append('ticker:' + symbol)
|
|
tickers = await self.watch_public_multiple(messageHashes, topics, params)
|
|
if self.newUpdates:
|
|
result: dict = {}
|
|
result[tickers['symbol']] = tickers
|
|
return result
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# default
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "SPOT",
|
|
# "channel": "ticker",
|
|
# "instId": "BTCUSDT"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "instId": "BTCUSDT",
|
|
# "lastPr": "43528.19",
|
|
# "open24h": "42267.78",
|
|
# "high24h": "44490.00",
|
|
# "low24h": "41401.53",
|
|
# "change24h": "0.03879",
|
|
# "bidPr": "43528",
|
|
# "askPr": "43528.01",
|
|
# "bidSz": "0.0334",
|
|
# "askSz": "0.1917",
|
|
# "baseVolume": "15002.4216",
|
|
# "quoteVolume": "648006446.7164",
|
|
# "openUtc": "44071.18",
|
|
# "changeUtc24h": "-0.01232",
|
|
# "ts": "1701842994338"
|
|
# }
|
|
# ],
|
|
# "ts": 1701842994341
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "highPrice24h": "120255.61",
|
|
# "lowPrice24h": "116145.88",
|
|
# "openPrice24h": "118919.38",
|
|
# "lastPrice": "119818.83",
|
|
# "turnover24h": "215859996.272276",
|
|
# "volume24h": "1819.756798",
|
|
# "bid1Price": "119811.26",
|
|
# "ask1Price": "119831.18",
|
|
# "bid1Size": "0.008732",
|
|
# "ask1Size": "0.004297",
|
|
# "price24hPcnt": "0.02002"
|
|
# }
|
|
# ],
|
|
# "ts": 1753230479687
|
|
# }
|
|
#
|
|
self.handle_bid_ask(client, message)
|
|
ticker = self.parse_ws_ticker(message)
|
|
symbol = ticker['symbol']
|
|
self.tickers[symbol] = ticker
|
|
messageHash = 'ticker:' + symbol
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def parse_ws_ticker(self, message, market=None):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "SPOT",
|
|
# "channel": "ticker",
|
|
# "instId": "BTCUSDT"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "instId": "BTCUSDT",
|
|
# "lastPr": "43528.19",
|
|
# "open24h": "42267.78",
|
|
# "high24h": "44490.00",
|
|
# "low24h": "41401.53",
|
|
# "change24h": "0.03879",
|
|
# "bidPr": "43528",
|
|
# "askPr": "43528.01",
|
|
# "bidSz": "0.0334",
|
|
# "askSz": "0.1917",
|
|
# "baseVolume": "15002.4216",
|
|
# "quoteVolume": "648006446.7164",
|
|
# "openUtc": "44071.18",
|
|
# "changeUtc24h": "-0.01232",
|
|
# "ts": "1701842994338"
|
|
# }
|
|
# ],
|
|
# "ts": 1701842994341
|
|
# }
|
|
#
|
|
# contract
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "USDT-FUTURES",
|
|
# "channel": "ticker",
|
|
# "instId": "BTCUSDT"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "instId": "BTCUSDT",
|
|
# "lastPr": "43480.4",
|
|
# "bidPr": "43476.3",
|
|
# "askPr": "43476.8",
|
|
# "bidSz": "0.1",
|
|
# "askSz": "3.055",
|
|
# "open24h": "42252.3",
|
|
# "high24h": "44518.2",
|
|
# "low24h": "41387.0",
|
|
# "change24h": "0.03875",
|
|
# "fundingRate": "0.000096",
|
|
# "nextFundingTime": "1701849600000",
|
|
# "markPrice": "43476.4",
|
|
# "indexPrice": "43478.4",
|
|
# "holdingAmount": "50670.787",
|
|
# "baseVolume": "120187.104",
|
|
# "quoteVolume": "5167385048.693",
|
|
# "openUtc": "44071.4",
|
|
# "symbolType": "1",
|
|
# "symbol": "BTCUSDT",
|
|
# "deliveryPrice": "0",
|
|
# "ts": "1701843962811"
|
|
# }
|
|
# ],
|
|
# "ts": 1701843962812
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "highPrice24h": "120255.61",
|
|
# "lowPrice24h": "116145.88",
|
|
# "openPrice24h": "118919.38",
|
|
# "lastPrice": "119818.83",
|
|
# "turnover24h": "215859996.272276",
|
|
# "volume24h": "1819.756798",
|
|
# "bid1Price": "119811.26",
|
|
# "ask1Price": "119831.18",
|
|
# "bid1Size": "0.008732",
|
|
# "ask1Size": "0.004297",
|
|
# "price24hPcnt": "0.02002"
|
|
# }
|
|
# ],
|
|
# "ts": 1753230479687
|
|
# }
|
|
#
|
|
arg = self.safe_value(message, 'arg', {})
|
|
data = self.safe_value(message, 'data', [])
|
|
ticker = self.safe_value(data, 0, {})
|
|
utaTimestamp = self.safe_integer(message, 'ts')
|
|
timestamp = self.safe_integer(ticker, 'ts', utaTimestamp)
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
marketType = 'spot' if (instType == 'spot') else 'contract'
|
|
utaMarketId = self.safe_string(arg, 'symbol')
|
|
marketId = self.safe_string(ticker, 'instId', utaMarketId)
|
|
market = self.safe_market(marketId, market, None, marketType)
|
|
close = self.safe_string_2(ticker, 'lastPr', 'lastPrice')
|
|
changeDecimal = self.safe_string(ticker, 'change24h', '')
|
|
change = self.safe_string(ticker, 'price24hPcnt', Precise.string_mul(changeDecimal, '100'))
|
|
return self.safe_ticker({
|
|
'symbol': market['symbol'],
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string_2(ticker, 'high24h', 'highPrice24h'),
|
|
'low': self.safe_string_2(ticker, 'low24h', 'lowPrice24h'),
|
|
'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'),
|
|
'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'),
|
|
'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'),
|
|
'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'),
|
|
'vwap': None,
|
|
'open': self.safe_string_2(ticker, 'open24h', 'openPrice24h'),
|
|
'close': close,
|
|
'last': close,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': change,
|
|
'average': None,
|
|
'baseVolume': self.safe_string_2(ticker, 'baseVolume', 'volume24h'),
|
|
'quoteVolume': self.safe_string_2(ticker, 'quoteVolume', 'turnover24h'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
watches best bid & ask for symbols
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Tickers-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Tickers-Channel
|
|
|
|
: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 boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
market = self.market(symbols[0])
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchBidsAsks', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
topics = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
marketInner = self.market(symbol)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
symbolOrInstId = 'symbol' if uta else 'instId'
|
|
args[topicOrChannel] = 'ticker'
|
|
args[symbolOrInstId] = marketInner['id']
|
|
topics.append(args)
|
|
messageHashes.append('bidask:' + symbol)
|
|
tickers = await self.watch_public_multiple(messageHashes, topics, params)
|
|
if self.newUpdates:
|
|
result: dict = {}
|
|
result[tickers['symbol']] = tickers
|
|
return result
|
|
return self.filter_by_array(self.bidsasks, 'symbol', symbols)
|
|
|
|
def handle_bid_ask(self, client: Client, message):
|
|
ticker = self.parse_ws_bid_ask(message)
|
|
symbol = ticker['symbol']
|
|
self.bidsasks[symbol] = ticker
|
|
messageHash = 'bidask:' + symbol
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def parse_ws_bid_ask(self, message, market=None):
|
|
arg = self.safe_value(message, 'arg', {})
|
|
data = self.safe_value(message, 'data', [])
|
|
ticker = self.safe_value(data, 0, {})
|
|
utaTimestamp = self.safe_integer(message, 'ts')
|
|
timestamp = self.safe_integer(ticker, 'ts', utaTimestamp)
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
marketType = 'spot' if (instType == 'spot') else 'contract'
|
|
utaMarketId = self.safe_string(arg, 'symbol')
|
|
marketId = self.safe_string(ticker, 'instId', utaMarketId)
|
|
market = self.safe_market(marketId, market, None, marketType)
|
|
return self.safe_ticker({
|
|
'symbol': market['symbol'],
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'ask': self.safe_string_2(ticker, 'askPr', 'ask1Price'),
|
|
'askVolume': self.safe_string_2(ticker, 'askSz', 'ask1Size'),
|
|
'bid': self.safe_string_2(ticker, 'bidPr', 'bid1Price'),
|
|
'bidVolume': self.safe_string_2(ticker, 'bidSz', 'bid1Size'),
|
|
'info': ticker,
|
|
}, 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, close price, and the volume of a market
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Candlesticks-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Candlesticks-Channel
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
timeframes = self.safe_value(self.options, 'timeframes')
|
|
interval = self.safe_string(timeframes, timeframe)
|
|
messageHash = None
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchOHLCV', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
if uta:
|
|
args['topic'] = 'kline'
|
|
args['symbol'] = market['id']
|
|
args['interval'] = interval
|
|
params = self.extend(params, {'uta': True})
|
|
messageHash = 'kline:' + symbol
|
|
else:
|
|
args['channel'] = 'candle' + interval
|
|
args['instId'] = market['id']
|
|
messageHash = 'candles:' + timeframe + ':' + symbol
|
|
ohlcv = await self.watch_public(messageHash, args, params)
|
|
if self.newUpdates:
|
|
limit = ohlcv.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
|
|
|
async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any:
|
|
"""
|
|
unsubscribe from the ohlcv channel
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Candlesticks-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Candlesticks-Channel
|
|
|
|
:param str symbol: unified symbol of the market to unwatch the ohlcv for
|
|
:param str [timeframe]: the period for the ratio, default is 1 minute
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
timeframes = self.safe_dict(self.options, 'timeframes')
|
|
interval = self.safe_string(timeframes, timeframe)
|
|
channel = None
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
instType = None
|
|
messageHash = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'unWatchOHLCV', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
if uta:
|
|
channel = 'kline'
|
|
args['topic'] = channel
|
|
args['symbol'] = market['id']
|
|
args['interval'] = interval
|
|
params = self.extend(params, {'uta': True})
|
|
params['interval'] = interval
|
|
messageHash = channel + symbol
|
|
else:
|
|
channel = 'candle' + interval
|
|
args['channel'] = channel
|
|
args['instId'] = market['id']
|
|
messageHash = 'candles:' + interval
|
|
return await self.un_watch_channel(symbol, channel, messageHash, params)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "SPOT",
|
|
# "channel": "candle1m",
|
|
# "instId": "BTCUSDT"
|
|
# },
|
|
# "data": [
|
|
# [
|
|
# "1701871620000",
|
|
# "44080.23",
|
|
# "44080.23",
|
|
# "44028.5",
|
|
# "44028.51",
|
|
# "9.9287",
|
|
# "437404.105512",
|
|
# "437404.105512"
|
|
# ],
|
|
# [
|
|
# "1701871680000",
|
|
# "44028.51",
|
|
# "44108.11",
|
|
# "44028.5",
|
|
# "44108.11",
|
|
# "17.139",
|
|
# "755436.870643",
|
|
# "755436.870643"
|
|
# ],
|
|
# ],
|
|
# "ts": 1701901610417
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "usdt-futures",
|
|
# "topic": "kline",
|
|
# "symbol": "BTCUSDT",
|
|
# "interval": "1m"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "start": "1755564480000",
|
|
# "open": "116286",
|
|
# "close": "116256.2",
|
|
# "high": "116310.2",
|
|
# "low": "116232.8",
|
|
# "volume": "39.7062",
|
|
# "turnover": "4616746.46654"
|
|
# },
|
|
# ],
|
|
# "ts": 1755594421877
|
|
# }
|
|
#
|
|
arg = self.safe_value(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
marketType = 'spot' if (instType == 'spot') else 'contract'
|
|
marketId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
symbol = market['symbol']
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
channel = self.safe_string_2(arg, 'channel', 'topic')
|
|
interval = self.safe_string(arg, 'interval')
|
|
isUta = None
|
|
if interval is None:
|
|
isUta = False
|
|
interval = channel.replace('candle', '')
|
|
else:
|
|
isUta = True
|
|
timeframes = self.safe_value(self.options, 'timeframes')
|
|
timeframe = self.find_timeframe(interval, timeframes)
|
|
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
|
|
data = self.safe_value(message, 'data', [])
|
|
for i in range(0, len(data)):
|
|
parsed = self.parse_ws_ohlcv(data[i], market)
|
|
stored.append(parsed)
|
|
messageHash = None
|
|
if isUta:
|
|
messageHash = 'kline:' + symbol
|
|
else:
|
|
messageHash = 'candles:' + timeframe + ':' + symbol
|
|
client.resolve(stored, messageHash)
|
|
|
|
def parse_ws_ohlcv(self, ohlcv, market=None) -> list:
|
|
#
|
|
# [
|
|
# "1701871620000", # timestamp
|
|
# "44080.23", # open
|
|
# "44080.23", # high
|
|
# "44028.5", # low
|
|
# "44028.51", # close
|
|
# "9.9287", # base volume
|
|
# "437404.105512", # quote volume
|
|
# "437404.105512" # USDT volume
|
|
# ]
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "start": "1755564480000",
|
|
# "open": "116286",
|
|
# "close": "116256.2",
|
|
# "high": "116310.2",
|
|
# "low": "116232.8",
|
|
# "volume": "39.7062",
|
|
# "turnover": "4616746.46654"
|
|
# }
|
|
#
|
|
volumeIndex = 6 if (market['inverse']) else 5
|
|
return [
|
|
self.safe_integer_2(ohlcv, 'start', 0),
|
|
self.safe_number_2(ohlcv, 'open', 1),
|
|
self.safe_number_2(ohlcv, 'high', 2),
|
|
self.safe_number_2(ohlcv, 'low', 3),
|
|
self.safe_number_2(ohlcv, 'close', 4),
|
|
self.safe_number_2(ohlcv, 'volume', volumeIndex),
|
|
]
|
|
|
|
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://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel
|
|
|
|
: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 boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
return await self.watch_order_book_for_symbols([symbol], limit, params)
|
|
|
|
async def un_watch_order_book(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unsubscribe from the orderbook channel
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.limit]: orderbook limit, default is None
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
channel = 'books'
|
|
limit = self.safe_integer(params, 'limit')
|
|
if (limit == 1) or (limit == 5) or (limit == 15) or (limit == 50):
|
|
params = self.omit(params, 'limit')
|
|
channel += str(limit)
|
|
return await self.un_watch_channel(symbol, channel, 'orderbook', params)
|
|
|
|
async def un_watch_channel(self, symbol: str, channel: str, messageHashTopic: str, params={}) -> Any:
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
messageHash = 'unsubscribe:' + messageHashTopic + ':' + market['symbol']
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'unWatchChannel', 'uta', False)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
if uta:
|
|
args['topic'] = channel
|
|
args['symbol'] = market['id']
|
|
args['interval'] = self.safe_string(params, 'interval', '1m')
|
|
params = self.extend(params, {'uta': True})
|
|
params = self.omit(params, 'interval')
|
|
else:
|
|
args['channel'] = channel
|
|
args['instId'] = market['id']
|
|
return await self.un_watch_public(messageHash, args, params)
|
|
|
|
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
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Depth-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/Order-Book-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/Order-Book-Channel
|
|
|
|
: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
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
channel = 'books'
|
|
incrementalFeed = True
|
|
if (limit == 1) or (limit == 5) or (limit == 15) or (limit == 50):
|
|
channel += str(limit)
|
|
incrementalFeed = False
|
|
topics = []
|
|
messageHashes = []
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'uta', False)
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
instType = None
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
symbolOrInstId = 'symbol' if uta else 'instId'
|
|
args[topicOrChannel] = channel
|
|
args[symbolOrInstId] = market['id']
|
|
topics.append(args)
|
|
messageHashes.append('orderbook:' + symbol)
|
|
if uta:
|
|
params['uta'] = True
|
|
orderbook = await self.watch_public_multiple(messageHashes, topics, params)
|
|
if incrementalFeed:
|
|
return orderbook.limit()
|
|
else:
|
|
return orderbook
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "action":"snapshot",
|
|
# "arg":{
|
|
# "instType":"SPOT",
|
|
# "channel":"books5",
|
|
# "instId":"BTCUSDT"
|
|
# },
|
|
# "data":[
|
|
# {
|
|
# "asks":[
|
|
# ["21041.11","0.0445"],
|
|
# ["21041.16","0.0411"],
|
|
# ["21041.21","0.0421"],
|
|
# ["21041.26","0.0811"],
|
|
# ["21041.65","1.9465"]
|
|
# ],
|
|
# "bids":[
|
|
# ["21040.76","0.0417"],
|
|
# ["21040.71","0.0434"],
|
|
# ["21040.66","0.1141"],
|
|
# ["21040.61","0.3004"],
|
|
# ["21040.60","1.3357"]
|
|
# ],
|
|
# "checksum": -1367582038,
|
|
# "ts":"1656413855484"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "usdt-futures", "topic": "books", "symbol": "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "a": [Array],
|
|
# "b": [Array],
|
|
# "checksum": 0,
|
|
# "pseq": 0,
|
|
# "seq": "1343064377779269632",
|
|
# "ts": "1755937421270"
|
|
# }
|
|
# ],
|
|
# "ts": 1755937421337
|
|
# }
|
|
#
|
|
arg = self.safe_value(message, 'arg')
|
|
channel = self.safe_string_2(arg, 'channel', 'topic')
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
marketType = 'spot' if (instType == 'spot') else 'contract'
|
|
marketId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
symbol = market['symbol']
|
|
messageHash = 'orderbook:' + symbol
|
|
data = self.safe_value(message, 'data')
|
|
rawOrderBook = self.safe_value(data, 0)
|
|
timestamp = self.safe_integer(rawOrderBook, 'ts')
|
|
incrementalBook = channel == 'books'
|
|
if incrementalBook:
|
|
# storedOrderBook = self.safe_value(self.orderbooks, symbol)
|
|
if not (symbol in self.orderbooks):
|
|
# ob = self.order_book({})
|
|
ob = self.counted_order_book({})
|
|
ob['symbol'] = symbol
|
|
self.orderbooks[symbol] = ob
|
|
storedOrderBook = self.orderbooks[symbol]
|
|
asks = self.safe_list_2(rawOrderBook, 'asks', 'a', [])
|
|
bids = self.safe_list_2(rawOrderBook, 'bids', 'b', [])
|
|
self.handle_deltas(storedOrderBook['asks'], asks)
|
|
self.handle_deltas(storedOrderBook['bids'], bids)
|
|
storedOrderBook['timestamp'] = timestamp
|
|
storedOrderBook['datetime'] = self.iso8601(timestamp)
|
|
checksum = self.handle_option('watchOrderBook', 'checksum', True)
|
|
isSnapshot = self.safe_string(message, 'action') == 'snapshot' # snapshot does not have a checksum
|
|
if not isSnapshot and checksum:
|
|
storedAsks = storedOrderBook['asks']
|
|
storedBids = storedOrderBook['bids']
|
|
asksLength = len(storedAsks)
|
|
bidsLength = len(storedBids)
|
|
payloadArray = []
|
|
for i in range(0, 25):
|
|
if i < bidsLength:
|
|
payloadArray.append(storedBids[i][2][0])
|
|
payloadArray.append(storedBids[i][2][1])
|
|
if i < asksLength:
|
|
payloadArray.append(storedAsks[i][2][0])
|
|
payloadArray.append(storedAsks[i][2][1])
|
|
payload = ':'.join(payloadArray)
|
|
calculatedChecksum = self.crc32(payload, True)
|
|
responseChecksum = self.safe_integer(rawOrderBook, 'checksum')
|
|
if calculatedChecksum != responseChecksum:
|
|
# if messageHash in client.subscriptions:
|
|
# # del client.subscriptions[messageHash]
|
|
# # del self.orderbooks[symbol]
|
|
# }
|
|
self.spawn(self.handle_check_sum_error, client, symbol, messageHash)
|
|
return
|
|
else:
|
|
orderbook = self.order_book({})
|
|
parsedOrderbook = self.parse_order_book(rawOrderBook, symbol, timestamp)
|
|
orderbook.reset(parsedOrderbook)
|
|
self.orderbooks[symbol] = orderbook
|
|
client.resolve(self.orderbooks[symbol], messageHash)
|
|
|
|
async def handle_check_sum_error(self, client: Client, symbol: str, messageHash: str):
|
|
await self.un_watch_order_book(symbol)
|
|
error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
|
|
client.reject(error, messageHash)
|
|
|
|
def handle_delta(self, bookside, delta):
|
|
bidAsk = self.parse_bid_ask(delta, 0, 1)
|
|
# we store the string representations in the orderbook for checksum calculation
|
|
# self simplifies the code for generating checksums do not need to do any complex number transformations
|
|
bidAsk.append(delta)
|
|
bookside.storeArray(bidAsk)
|
|
|
|
def handle_deltas(self, bookside, deltas):
|
|
for i in range(0, len(deltas)):
|
|
self.handle_delta(bookside, deltas[i])
|
|
|
|
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://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel
|
|
|
|
: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
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
return await self.watch_trades_for_symbols([symbol], since, limit, params)
|
|
|
|
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
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel
|
|
|
|
: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
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength == 0:
|
|
raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols')
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'uta', False)
|
|
topics = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
instType = None
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
symbolOrInstId = 'symbol' if uta else 'instId'
|
|
args[topicOrChannel] = 'publicTrade' if uta else 'trade'
|
|
args[symbolOrInstId] = market['id']
|
|
topics.append(args)
|
|
messageHashes.append('trade:' + symbol)
|
|
if uta:
|
|
params = self.extend(params, {'uta': True})
|
|
trades = await self.watch_public_multiple(messageHashes, topics, params)
|
|
if self.newUpdates:
|
|
first = self.safe_value(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
result = self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
if self.handle_option('watchTrades', 'ignoreDuplicates', True):
|
|
filtered = self.remove_repeated_trades_from_array(result)
|
|
filtered = self.sort_by(filtered, 'timestamp')
|
|
return filtered
|
|
return result
|
|
|
|
async def un_watch_trades(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unsubscribe from the trades channel
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/public/Trades-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/public/New-Trades-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/public/New-Trades-Channel
|
|
|
|
:param str symbol: unified symbol of the market to unwatch the trades for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns any: status of the unwatch request
|
|
"""
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'unWatchTrades', 'uta', False)
|
|
channelTopic = 'publicTrade' if uta else 'trade'
|
|
return await self.un_watch_channel(symbol, channelTopic, 'trade', params)
|
|
|
|
def handle_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "SPOT", "channel": "trade", "instId": "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "ts": "1701910980366",
|
|
# "price": "43854.01",
|
|
# "size": "0.0535",
|
|
# "side": "buy",
|
|
# "tradeId": "1116461060594286593"
|
|
# },
|
|
# ],
|
|
# "ts": 1701910980730
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "spot", "topic": "publicTrade", "symbol": "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "T": "1756287827920",
|
|
# "P": "110878.5",
|
|
# "v": "0.07",
|
|
# "S": "buy",
|
|
# "L": "1344534089797185550"
|
|
# "i": "1344534089797185549"
|
|
# },
|
|
# ],
|
|
# "ts": 1701910980730
|
|
# }
|
|
#
|
|
arg = self.safe_value(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
marketType = 'spot' if (instType == 'spot') else 'contract'
|
|
marketId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
symbol = market['symbol']
|
|
stored = self.safe_value(self.trades, symbol)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
stored = ArrayCache(limit)
|
|
self.trades[symbol] = stored
|
|
data = self.safe_list(message, 'data', [])
|
|
length = len(data)
|
|
# fix chronological order by reversing
|
|
for i in range(0, length):
|
|
index = length - i - 1
|
|
rawTrade = data[index]
|
|
parsed = self.parse_ws_trade(rawTrade, market)
|
|
stored.append(parsed)
|
|
messageHash = 'trade:' + symbol
|
|
client.resolve(stored, messageHash)
|
|
|
|
def parse_ws_trade(self, trade, market=None):
|
|
#
|
|
# {
|
|
# "ts": "1701910980366",
|
|
# "price": "43854.01",
|
|
# "size": "0.0535",
|
|
# "side": "buy",
|
|
# "tradeId": "1116461060594286593"
|
|
# }
|
|
# swap private
|
|
#
|
|
# {
|
|
# "orderId": "1169142761031114781",
|
|
# "tradeId": "1169142761312637004",
|
|
# "symbol": "LTCUSDT",
|
|
# "orderType": "market",
|
|
# "side": "buy",
|
|
# "price": "80.87",
|
|
# "baseVolume": "0.1",
|
|
# "quoteVolume": "8.087",
|
|
# "profit": "0",
|
|
# "tradeSide": "open",
|
|
# "posMode": "hedge_mode",
|
|
# "tradeScope": "taker",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "deduction": "no",
|
|
# "totalDeductionFee": "0",
|
|
# "totalFee": "-0.0048522"
|
|
# }
|
|
# ],
|
|
# "cTime": "1714471276596",
|
|
# "uTime": "1714471276596"
|
|
# }
|
|
# spot private
|
|
# {
|
|
# "orderId": "1169142457356959747",
|
|
# "tradeId": "1169142457636958209",
|
|
# "symbol": "LTCUSDT",
|
|
# "orderType": "market",
|
|
# "side": "buy",
|
|
# "priceAvg": "81.069",
|
|
# "size": "0.074",
|
|
# "amount": "5.999106",
|
|
# "tradeScope": "taker",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "LTC",
|
|
# "deduction": "no",
|
|
# "totalDeductionFee": "0",
|
|
# "totalFee": "0.000074"
|
|
# }
|
|
# ],
|
|
# "cTime": "1714471204194",
|
|
# "uTime": "1714471204194"
|
|
# }
|
|
#
|
|
# uta private
|
|
#
|
|
# {
|
|
# "symbol": "BTCUSDT",
|
|
# "orderType": "market",
|
|
# "updatedTime": "1736378720623",
|
|
# "side": "buy",
|
|
# "orderId": "1288888888888888888",
|
|
# "execPnl": "0",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "fee": "0.569958"
|
|
# }
|
|
# ],
|
|
# "execTime": "1736378720623",
|
|
# "tradeScope": "taker",
|
|
# "tradeSide": "open",
|
|
# "execId": "1288888888888888888",
|
|
# "execLinkId": "1288888888888888888",
|
|
# "execPrice": "94993",
|
|
# "holdSide": "long",
|
|
# "execValue": "949.93",
|
|
# "category": "USDT-FUTURES",
|
|
# "execQty": "0.01",
|
|
# "clientOid": "1288888888888888889"
|
|
# uta
|
|
#
|
|
# {
|
|
# "i": "1344534089797185549", # Fill execution ID
|
|
# "L": "1344534089797185550", # Execution correlation ID
|
|
# "p": "110878.5", # Fill price
|
|
# "v": "0.07", # Fill size
|
|
# "S": "buy", # Fill side
|
|
# "T": "1756287827920" # Fill timestamp
|
|
# }
|
|
#
|
|
instId = self.safe_string_2(trade, 'symbol', 'instId')
|
|
posMode = self.safe_string(trade, 'posMode')
|
|
category = self.safe_string(trade, 'category')
|
|
defaultType = None
|
|
if category is not None:
|
|
defaultType = 'contract' if (category != 'SPOT') else 'spot'
|
|
else:
|
|
defaultType = 'contract' if (posMode is not None) else 'spot'
|
|
if market is None:
|
|
market = self.safe_market(instId, None, None, defaultType)
|
|
timestamp = self.safe_integer_n(trade, ['uTime', 'cTime', 'ts', 'T', 'execTime'])
|
|
feeDetail = self.safe_list(trade, 'feeDetail', [])
|
|
first = self.safe_dict(feeDetail, 0)
|
|
fee = None
|
|
if first is not None:
|
|
feeCurrencyId = self.safe_string(first, 'feeCoin')
|
|
feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
|
|
fee = {
|
|
'cost': Precise.string_abs(self.safe_string_2(first, 'totalFee', 'fee')),
|
|
'currency': feeCurrencyCode,
|
|
}
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'id': self.safe_string_n(trade, ['tradeId', 'i', 'execId']),
|
|
'order': self.safe_string_2(trade, 'orderId', 'L'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'type': self.safe_string(trade, 'orderType'),
|
|
'side': self.safe_string_2(trade, 'side', 'S'),
|
|
'takerOrMaker': self.safe_string(trade, 'tradeScope'),
|
|
'price': self.safe_string_n(trade, ['priceAvg', 'price', 'execPrice', 'P']),
|
|
'amount': self.safe_string_n(trade, ['size', 'baseVolume', 'execQty', 'v']),
|
|
'cost': self.safe_string_n(trade, ['amount', 'quoteVolume', 'execValue']),
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
|
"""
|
|
watch all open positions
|
|
|
|
https://www.bitget.com/api-doc/contract/websocket/private/Positions-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/private/Positions-Channel
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param int [since]: the earliest time in ms to fetch positions for
|
|
:param int [limit]: the maximum number of positions to retrieve
|
|
:param dict params: extra parameters specific to the exchange API endpoint
|
|
:param str [params.instType]: one of 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES', default is 'USDT-FUTURES'
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
messageHash = ''
|
|
subscriptionHash = 'positions'
|
|
instType = 'USDT-FUTURES'
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchPositions', 'uta', False)
|
|
symbols = self.market_symbols(symbols)
|
|
if not self.is_empty(symbols):
|
|
market = self.get_market_from_symbols(symbols)
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
if uta:
|
|
instType = 'UTA'
|
|
messageHash = instType + ':positions' + messageHash
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
channel = 'position' if uta else 'positions'
|
|
args[topicOrChannel] = channel
|
|
if not uta:
|
|
args['instId'] = 'default'
|
|
else:
|
|
params = self.extend(params, {'uta': True})
|
|
newPositions = await self.watch_private(messageHash, subscriptionHash, args, params)
|
|
if self.newUpdates:
|
|
return newPositions
|
|
return self.filter_by_symbols_since_limit(newPositions, symbols, since, limit, True)
|
|
|
|
def handle_positions(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "USDT-FUTURES",
|
|
# "channel": "positions",
|
|
# "instId": "default"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "posId": "926036334386778112",
|
|
# "instId": "BTCUSDT",
|
|
# "marginCoin": "USDT",
|
|
# "marginSize": "2.19245",
|
|
# "marginMode": "crossed",
|
|
# "holdSide": "long",
|
|
# "posMode": "hedge_mode",
|
|
# "total": "0.001",
|
|
# "available": "0.001",
|
|
# "frozen": "0",
|
|
# "openPriceAvg": "43849",
|
|
# "leverage": 20,
|
|
# "achievedProfits": "0",
|
|
# "unrealizedPL": "-0.0032",
|
|
# "unrealizedPLR": "-0.00145955438",
|
|
# "liquidationPrice": "17629.684814834",
|
|
# "keepMarginRate": "0.004",
|
|
# "marginRate": "0.007634649185",
|
|
# "cTime": "1652331666985",
|
|
# "uTime": "1701913016923",
|
|
# "autoMargin": "off"
|
|
# },
|
|
# ...
|
|
# ]
|
|
# "ts": 1701913043767
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "symbol": "BTCUSDT",
|
|
# "leverage": "20",
|
|
# "openFeeTotal": "",
|
|
# "mmr": "",
|
|
# "breakEvenPrice": "",
|
|
# "available": "0",
|
|
# "liqPrice": "",
|
|
# "marginMode": "crossed",
|
|
# "unrealisedPnl": "0",
|
|
# "markPrice": "94987.1",
|
|
# "createdTime": "1736378720620",
|
|
# "avgPrice": "0",
|
|
# "totalFundingFee": "0",
|
|
# "updatedTime": "1736378720620",
|
|
# "marginCoin": "USDT",
|
|
# "frozen": "0",
|
|
# "profitRate": "",
|
|
# "closeFeeTotal": "",
|
|
# "marginSize": "0",
|
|
# "curRealisedPnl": "0",
|
|
# "size": "0",
|
|
# "positionStatus": "ended",
|
|
# "posSide": "long",
|
|
# "holdMode": "hedge_mode"
|
|
# }
|
|
# ],
|
|
# "arg": {
|
|
# "instType": "UTA",
|
|
# "topic": "position"
|
|
# },
|
|
# "action": "snapshot",
|
|
# "ts": 1730711666652
|
|
# }
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string(arg, 'instType', '')
|
|
if self.positions is None:
|
|
self.positions = {}
|
|
action = self.safe_string(message, 'action')
|
|
if not (instType in self.positions) or (action == 'snapshot'):
|
|
self.positions[instType] = ArrayCacheBySymbolBySide()
|
|
cache = self.positions[instType]
|
|
rawPositions = self.safe_list(message, 'data', [])
|
|
newPositions = []
|
|
for i in range(0, len(rawPositions)):
|
|
rawPosition = rawPositions[i]
|
|
marketId = self.safe_string_2(rawPosition, 'instId', 'symbol')
|
|
market = self.safe_market(marketId, None, None, 'contract')
|
|
position = self.parse_ws_position(rawPosition, market)
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
messageHashes = self.find_message_hashes(client, instType + ':positions::')
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
parts = messageHash.split('::')
|
|
symbolsString = parts[1]
|
|
symbols = symbolsString.split(',')
|
|
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
|
if not self.is_empty(positions):
|
|
client.resolve(positions, messageHash)
|
|
client.resolve(newPositions, instType + ':positions')
|
|
|
|
def parse_ws_position(self, position, market=None):
|
|
#
|
|
# {
|
|
# "posId": "926036334386778112",
|
|
# "instId": "BTCUSDT",
|
|
# "marginCoin": "USDT",
|
|
# "marginSize": "2.19245",
|
|
# "marginMode": "crossed",
|
|
# "holdSide": "long",
|
|
# "posMode": "hedge_mode",
|
|
# "total": "0.001",
|
|
# "available": "0.001",
|
|
# "frozen": "0",
|
|
# "openPriceAvg": "43849",
|
|
# "leverage": 20,
|
|
# "achievedProfits": "0",
|
|
# "unrealizedPL": "-0.0032",
|
|
# "unrealizedPLR": "-0.00145955438",
|
|
# "liquidationPrice": "17629.684814834",
|
|
# "keepMarginRate": "0.004",
|
|
# "marginRate": "0.007634649185",
|
|
# "cTime": "1652331666985",
|
|
# "uTime": "1701913016923",
|
|
# "autoMargin": "off"
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "symbol": "BTCUSDT",
|
|
# "leverage": "20",
|
|
# "openFeeTotal": "",
|
|
# "mmr": "",
|
|
# "breakEvenPrice": "",
|
|
# "available": "0",
|
|
# "liqPrice": "",
|
|
# "marginMode": "crossed",
|
|
# "unrealisedPnl": "0",
|
|
# "markPrice": "94987.1",
|
|
# "createdTime": "1736378720620",
|
|
# "avgPrice": "0",
|
|
# "totalFundingFee": "0",
|
|
# "updatedTime": "1736378720620",
|
|
# "marginCoin": "USDT",
|
|
# "frozen": "0",
|
|
# "profitRate": "",
|
|
# "closeFeeTotal": "",
|
|
# "marginSize": "0",
|
|
# "curRealisedPnl": "0",
|
|
# "size": "0",
|
|
# "positionStatus": "ended",
|
|
# "posSide": "long",
|
|
# "holdMode": "hedge_mode"
|
|
# }
|
|
#
|
|
marketId = self.safe_string_2(position, 'instId', 'symbol')
|
|
marginModeId = self.safe_string(position, 'marginMode')
|
|
marginMode = self.get_supported_mapping(marginModeId, {
|
|
'crossed': 'cross',
|
|
'isolated': 'isolated',
|
|
})
|
|
hedgedId = self.safe_string_2(position, 'posMode', 'holdMode')
|
|
hedged = True if (hedgedId == 'hedge_mode') else False
|
|
timestamp = self.safe_integer_n(position, ['updatedTime', 'uTime', 'cTime', 'createdTime'])
|
|
percentageDecimal = self.safe_string_2(position, 'unrealizedPLR', 'profitRate')
|
|
percentage = Precise.string_mul(percentageDecimal, '100')
|
|
contractSize = None
|
|
if market is not None:
|
|
contractSize = market['contractSize']
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': self.safe_string(position, 'posId'),
|
|
'symbol': self.safe_symbol(marketId, market, None, 'contract'),
|
|
'notional': None,
|
|
'marginMode': marginMode,
|
|
'liquidationPrice': self.safe_number_2(position, 'liquidationPrice', 'liqPrice'),
|
|
'entryPrice': self.safe_number_2(position, 'openPriceAvg', 'avgPrice'),
|
|
'unrealizedPnl': self.safe_number_2(position, 'unrealizedPL', 'unrealisedPnl'),
|
|
'percentage': self.parse_number(percentage),
|
|
'contracts': self.safe_number_2(position, 'total', 'size'),
|
|
'contractSize': contractSize,
|
|
'markPrice': self.safe_number(position, 'markPrice'),
|
|
'side': self.safe_string_2(position, 'holdSide', 'posSide'),
|
|
'hedged': hedged,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': self.safe_number_2(position, 'keepMarginRate', 'mmr'),
|
|
'collateral': self.safe_number(position, 'available'),
|
|
'initialMargin': self.safe_number(position, 'marginSize'),
|
|
'initialMarginPercentage': None,
|
|
'leverage': self.safe_number(position, 'leverage'),
|
|
'marginRatio': self.safe_number(position, 'marginRate'),
|
|
})
|
|
|
|
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://www.bitget.com/api-doc/spot/websocket/private/Order-Channel
|
|
https://www.bitget.com/api-doc/spot/websocket/private/Plan-Order-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/private/Order-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/private/Plan-Order-Channel
|
|
https://www.bitget.com/api-doc/margin/cross/websocket/private/Cross-Orders
|
|
https://www.bitget.com/api-doc/margin/isolated/websocket/private/Isolate-Orders
|
|
https://www.bitget.com/api-doc/uta/websocket/private/Order-Channel
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.trigger]: *contract only* set to True for watching trigger orders
|
|
:param str [params.marginMode]: 'isolated' or 'cross' for watching spot margin orders]
|
|
:param str [params.type]: 'spot', 'swap'
|
|
:param str [params.subType]: 'linear', 'inverse'
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
marketId = None
|
|
isTrigger = None
|
|
isTrigger, params = self.is_trigger_order(params)
|
|
messageHash = 'triggerOrder' if (isTrigger) else 'order'
|
|
subscriptionHash = 'order:trades'
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
marketId = market['id']
|
|
messageHash = messageHash + ':' + symbol
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchOrders', 'uta', False)
|
|
productType = self.safe_string(params, 'productType')
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchOrders', market, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchOrders', market, params, 'linear')
|
|
if (type == 'spot' or type == 'margin') and (symbol is None):
|
|
marketId = 'default'
|
|
if (productType is None) and (type != 'spot') and (symbol is None):
|
|
messageHash = messageHash + ':' + subType
|
|
elif productType == 'USDT-FUTURES':
|
|
messageHash = messageHash + ':linear'
|
|
elif productType == 'COIN-FUTURES':
|
|
messageHash = messageHash + ':inverse'
|
|
elif productType == 'USDC-FUTURES':
|
|
messageHash = messageHash + ':usdcfutures' # non unified channel
|
|
instType = None
|
|
if market is None and type == 'spot':
|
|
instType = 'SPOT'
|
|
else:
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
if type == 'spot' and (symbol is not None):
|
|
subscriptionHash = subscriptionHash + ':' + symbol
|
|
if isTrigger:
|
|
subscriptionHash = subscriptionHash + ':stop' # we don't want to re-use the same subscription hash for stop orders
|
|
instId = marketId if (type == 'spot' or type == 'margin') else 'default' # different from other streams here the 'rest' id is required for spot markets, contract markets require default here
|
|
channel = 'orders-algo' if isTrigger else 'orders'
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('watchOrders', params)
|
|
if marginMode is not None:
|
|
instType = 'MARGIN'
|
|
messageHash = messageHash + ':' + marginMode
|
|
if marginMode == 'isolated':
|
|
channel = 'orders-isolated'
|
|
else:
|
|
channel = 'orders-crossed'
|
|
if uta:
|
|
instType = 'UTA'
|
|
channel = 'order'
|
|
subscriptionHash = subscriptionHash + ':' + instType
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
args[topicOrChannel] = channel
|
|
if not uta:
|
|
args['instId'] = instId
|
|
else:
|
|
params = self.extend(params, {'uta': True})
|
|
orders = await self.watch_private(messageHash, subscriptionHash, args, params)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
|
|
|
|
def handle_order(self, client: Client, message):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "SPOT", "channel": "orders", "instId": "BTCUSDT"},
|
|
# "data": [
|
|
# # see all examples in parseWsOrder
|
|
# ],
|
|
# "ts": 1701923297285
|
|
# }
|
|
#
|
|
# contract
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "USDT-FUTURES", "channel": "orders", "instId": "default"},
|
|
# "data": [
|
|
# # see all examples in parseWsOrder
|
|
# ],
|
|
# "ts": 1701920595879
|
|
# }
|
|
#
|
|
# isolated and cross margin
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "MARGIN", "channel": "orders-crossed", "instId": "BTCUSDT"},
|
|
# "data": [
|
|
# # see examples in parseWsOrder
|
|
# ],
|
|
# "ts": 1701923982497
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "UTA",
|
|
# "topic": "order"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "category": "usdt-futures",
|
|
# "symbol": "BTCUSDT",
|
|
# "orderId": "xxx",
|
|
# "clientOid": "xxx",
|
|
# "price": "",
|
|
# "qty": "0.001",
|
|
# "amount": "1000",
|
|
# "holdMode": "hedge_mode",
|
|
# "holdSide": "long",
|
|
# "tradeSide": "open",
|
|
# "orderType": "market",
|
|
# "timeInForce": "gtc",
|
|
# "side": "buy",
|
|
# "marginMode": "crossed",
|
|
# "marginCoin": "USDT",
|
|
# "reduceOnly": "no",
|
|
# "cumExecQty": "0.001",
|
|
# "cumExecValue": "83.1315",
|
|
# "avgPrice": "83131.5",
|
|
# "totalProfit": "0",
|
|
# "orderStatus": "filled",
|
|
# "cancelReason": "",
|
|
# "leverage": "20",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "fee": "0.0332526"
|
|
# }
|
|
# ],
|
|
# "createdTime": "1742367838101",
|
|
# "updatedTime": "1742367838115",
|
|
# "stpMode": "none"
|
|
# }
|
|
# ],
|
|
# "ts": 1742367838124
|
|
# }
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
channel = self.safe_string_2(arg, 'channel', 'topic')
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
argInstId = self.safe_string(arg, 'instId')
|
|
marketType = None
|
|
if instType == 'spot':
|
|
marketType = 'spot'
|
|
elif instType == 'margin':
|
|
marketType = 'spot'
|
|
else:
|
|
marketType = 'contract'
|
|
data = self.safe_list(message, 'data', [])
|
|
first = self.safe_dict(data, 0, {})
|
|
category = self.safe_string_lower(first, 'category', instType)
|
|
isLinearSwap = (category == 'usdt-futures')
|
|
isInverseSwap = (category == 'coin-futures')
|
|
isUSDCFutures = (category == 'usdc-futures')
|
|
if self.orders is None:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
self.triggerOrders = ArrayCacheBySymbolById(limit)
|
|
isTrigger = (channel == 'orders-algo') or (channel == 'ordersAlgo')
|
|
stored = self.triggerOrders if isTrigger else self.orders
|
|
messageHash = 'triggerOrder' if isTrigger else 'order'
|
|
marketSymbols: dict = {}
|
|
for i in range(0, len(data)):
|
|
order = data[i]
|
|
marketId = self.safe_string_2(order, 'instId', 'symbol', argInstId)
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
parsed = self.parse_ws_order(order, market)
|
|
stored.append(parsed)
|
|
symbol = parsed['symbol']
|
|
marketSymbols[symbol] = True
|
|
keys = list(marketSymbols.keys())
|
|
for i in range(0, len(keys)):
|
|
symbol = keys[i]
|
|
innerMessageHash = messageHash + ':' + symbol
|
|
if channel == 'orders-crossed':
|
|
innerMessageHash = innerMessageHash + ':cross'
|
|
elif channel == 'orders-isolated':
|
|
innerMessageHash = innerMessageHash + ':isolated'
|
|
client.resolve(stored, innerMessageHash)
|
|
client.resolve(stored, messageHash)
|
|
if isLinearSwap:
|
|
client.resolve(stored, 'order:linear')
|
|
if isInverseSwap:
|
|
client.resolve(stored, 'order:inverse')
|
|
if isUSDCFutures:
|
|
client.resolve(stored, 'order:usdcfutures')
|
|
|
|
def parse_ws_order(self, order, market=None):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# instId: 'EOSUSDT',
|
|
# orderId: '1171779081105780739',
|
|
# price: '0.81075', # limit price, field not present for market orders
|
|
# clientOid: 'a2330139-1d04-4d78-98be-07de3cfd1055',
|
|
# notional: '5.675250', # self is not cost! but notional
|
|
# newSize: '7.0000', # self is not cost! quanity(for limit order or market sell) or cost(for market buy order)
|
|
# size: '5.6752', # self is not cost, neither quanity, but notional! self field for "spot" can be ignored at all
|
|
# # Note: for limit order(even filled) we don't have cost value in response, only in market order
|
|
# orderType: 'limit', # limit, market
|
|
# force: 'gtc',
|
|
# side: 'buy',
|
|
# accBaseVolume: '0.0000', # in case of 'filled', self would be set(for limit orders, self is the only indicator of the amount filled)
|
|
# priceAvg: '0.00000', # in case of 'filled', self would be set
|
|
# status: 'live', # live, filled, partially_filled
|
|
# cTime: '1715099824215',
|
|
# uTime: '1715099824215',
|
|
# feeDetail: [],
|
|
# enterPointSource: 'API'
|
|
# #### trigger order has these additional fields: ####
|
|
# "triggerPrice": "35100",
|
|
# "price": "35100", # self is same price
|
|
# "executePrice": "35123", # self is limit price
|
|
# "triggerType": "fill_price",
|
|
# "planType": "amount",
|
|
# #### in case order had a partial fill: ####
|
|
# fillPrice: '35123',
|
|
# tradeId: '1171775539946528779',
|
|
# baseVolume: '7', # field present in market order
|
|
# fillTime: '1715098979937',
|
|
# fillFee: '-0.0069987',
|
|
# fillFeeCoin: 'BTC',
|
|
# tradeScope: 'T',
|
|
# }
|
|
#
|
|
# contract
|
|
#
|
|
# {
|
|
# accBaseVolume: '0', # total amount filled during lifetime for order
|
|
# cTime: '1715065875539',
|
|
# clientOid: '1171636690041344003',
|
|
# enterPointSource: 'API',
|
|
# feeDetail: [{
|
|
# "feeCoin": "USDT",
|
|
# "fee": "-0.162003"
|
|
# }],
|
|
# force: 'gtc',
|
|
# instId: 'SEOSSUSDT',
|
|
# leverage: '10',
|
|
# marginCoin: 'USDT',
|
|
# marginMode: 'crossed',
|
|
# notionalUsd: '10.4468',
|
|
# orderId: '1171636690028761089',
|
|
# orderType: 'market',
|
|
# posMode: 'hedge_mode', # one_way_mode, hedge_mode
|
|
# posSide: 'short', # short, long, net
|
|
# price: '0', # zero for market order
|
|
# reduceOnly: 'no',
|
|
# side: 'sell',
|
|
# size: '13', # self is contracts amount
|
|
# status: 'live', # live, filled, cancelled
|
|
# tradeSide: 'open',
|
|
# uTime: '1715065875539'
|
|
# #### when filled order is incoming, these additional fields are present too: ###
|
|
# baseVolume: '9', # amount filled for the incoming update/trade
|
|
# accBaseVolume: '13', # i.e. 9 has been filled from 13 amount(self value is same as 'size')
|
|
# fillFee: '-0.0062712',
|
|
# fillFeeCoin: 'SUSDT',
|
|
# fillNotionalUsd: '10.452',
|
|
# fillPrice: '0.804',
|
|
# fillTime: '1715065875605',
|
|
# pnl: '0',
|
|
# priceAvg: '0.804',
|
|
# tradeId: '1171636690314407937',
|
|
# tradeScope: 'T',
|
|
# #### trigger order has these additional fields:
|
|
# "triggerPrice": "0.800000000",
|
|
# "price": "0.800000000", # <-- self is same price, actual limit-price is not present in initial response
|
|
# "triggerType": "mark_price",
|
|
# "triggerTime": "1715082796679",
|
|
# "planType": "pl",
|
|
# "actualSize": "0.000000000",
|
|
# "stopSurplusTriggerType": "fill_price",
|
|
# "stopLossTriggerType": "fill_price",
|
|
# }
|
|
#
|
|
# isolated and cross margin
|
|
#
|
|
# {
|
|
# enterPointSource: "web",
|
|
# feeDetail: [
|
|
# {
|
|
# feeCoin: "AAVE",
|
|
# deduction: "no",
|
|
# totalDeductionFee: "0",
|
|
# totalFee: "-0.00010740",
|
|
# },
|
|
# ],
|
|
# force: "gtc",
|
|
# orderType: "limit",
|
|
# price: "93.170000000",
|
|
# fillPrice: "93.170000000",
|
|
# baseSize: "0.110600000", # total amount of order
|
|
# quoteSize: "10.304602000", # total cost of order(independently if order is filled or pending)
|
|
# baseVolume: "0.107400000", # filled amount of order(during order's lifecycle, and not for self specific incoming update)
|
|
# fillTotalAmount: "10.006458000", # filled cost of order(during order's lifecycle, and not for self specific incoming update)
|
|
# side: "buy",
|
|
# status: "partially_filled",
|
|
# cTime: "1717875017306",
|
|
# clientOid: "b57afe789a06454e9c560a2aab7f7201",
|
|
# loanType: "auto-loan",
|
|
# orderId: "1183419084588060673",
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "category": "usdt-futures",
|
|
# "symbol": "BTCUSDT",
|
|
# "orderId": "xxx",
|
|
# "clientOid": "xxx",
|
|
# "price": "",
|
|
# "qty": "0.001",
|
|
# "amount": "1000",
|
|
# "holdMode": "hedge_mode",
|
|
# "holdSide": "long",
|
|
# "tradeSide": "open",
|
|
# "orderType": "market",
|
|
# "timeInForce": "gtc",
|
|
# "side": "buy",
|
|
# "marginMode": "crossed",
|
|
# "marginCoin": "USDT",
|
|
# "reduceOnly": "no",
|
|
# "cumExecQty": "0.001",
|
|
# "cumExecValue": "83.1315",
|
|
# "avgPrice": "83131.5",
|
|
# "totalProfit": "0",
|
|
# "orderStatus": "filled",
|
|
# "cancelReason": "",
|
|
# "leverage": "20",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "fee": "0.0332526"
|
|
# }
|
|
# ],
|
|
# "createdTime": "1742367838101",
|
|
# "updatedTime": "1742367838115",
|
|
# "stpMode": "none"
|
|
# }
|
|
#
|
|
isSpot = not ('posMode' in order)
|
|
isMargin = ('loanType' in order)
|
|
category = self.safe_string_lower(order, 'category')
|
|
if category == 'spot':
|
|
isSpot = True
|
|
if category == 'margin':
|
|
isMargin = True
|
|
marketId = self.safe_string_2(order, 'instId', 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer_2(order, 'cTime', 'createdTime')
|
|
symbol = market['symbol']
|
|
rawStatus = self.safe_string_2(order, 'status', 'orderStatus')
|
|
orderFee = self.safe_value(order, 'feeDetail', [])
|
|
fee = self.safe_value(orderFee, 0)
|
|
feeAmount = self.safe_string(fee, 'fee')
|
|
feeObject = None
|
|
if feeAmount is not None:
|
|
feeCurrency = self.safe_string(fee, 'feeCoin')
|
|
feeObject = {
|
|
'cost': self.parse_number(Precise.string_abs(feeAmount)),
|
|
'currency': self.safe_currency_code(feeCurrency),
|
|
}
|
|
triggerPrice = self.safe_number(order, 'triggerPrice')
|
|
isTriggerOrder = (triggerPrice is not None)
|
|
price = None
|
|
if not isTriggerOrder:
|
|
price = self.safe_number(order, 'price')
|
|
elif isSpot and isTriggerOrder:
|
|
# for spot trigger order, limit price is self
|
|
price = self.safe_number(order, 'executePrice')
|
|
avgPrice = self.omit_zero(self.safe_string_lower_n(order, ['priceAvg', 'fillPrice', 'avgPrice']))
|
|
side = self.safe_string(order, 'side')
|
|
type = self.safe_string(order, 'orderType')
|
|
accBaseVolume = self.omit_zero(self.safe_string_2(order, 'accBaseVolume', 'cumExecQty'))
|
|
newSizeValue = self.omit_zero(self.safe_string_2(order, 'newSize', 'cumExecValue'))
|
|
isMarketOrder = (type == 'market')
|
|
isBuy = (side == 'buy')
|
|
totalAmount = None
|
|
filledAmount = None
|
|
cost = None
|
|
remaining = None
|
|
totalFilled = self.safe_string_2(order, 'accBaseVolume', 'cumExecQty')
|
|
if isSpot:
|
|
if isMargin:
|
|
totalAmount = self.safe_string_2(order, 'baseSize', 'qty')
|
|
totalFilled = self.safe_string_2(order, 'baseVolume', 'cumExecQty')
|
|
cost = self.safe_string_2(order, 'fillTotalAmount', 'cumExecValue')
|
|
else:
|
|
partialFillAmount = self.safe_string(order, 'baseVolume')
|
|
if partialFillAmount is not None:
|
|
filledAmount = partialFillAmount
|
|
else:
|
|
filledAmount = totalFilled
|
|
if isMarketOrder:
|
|
if isBuy:
|
|
totalAmount = accBaseVolume
|
|
cost = newSizeValue
|
|
else:
|
|
totalAmount = newSizeValue
|
|
# we don't have cost for market-sell order
|
|
else:
|
|
totalAmount = self.safe_string_2(order, 'newSize', 'qty')
|
|
# we don't have cost for limit order
|
|
else:
|
|
# baseVolume should not be used for "amount" for contracts !
|
|
filledAmount = self.safe_string_2(order, 'baseVolume', 'cumExecQty')
|
|
totalAmount = self.safe_string_2(order, 'size', 'qty')
|
|
cost = self.safe_string_2(order, 'fillNotionalUsd', 'cumExecValue')
|
|
remaining = Precise.string_sub(totalAmount, totalFilled)
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': self.safe_string(order, 'orderId'),
|
|
'clientOrderId': self.safe_string(order, 'clientOid'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': self.safe_integer_2(order, 'uTime', 'updatedTime'),
|
|
'type': type,
|
|
'timeInForce': self.safe_string_upper_2(order, 'force', 'timeInForce'),
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': triggerPrice,
|
|
'amount': totalAmount,
|
|
'cost': cost,
|
|
'average': avgPrice,
|
|
'filled': filledAmount,
|
|
'remaining': remaining,
|
|
'status': self.parse_ws_order_status(rawStatus),
|
|
'fee': feeObject,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
def parse_ws_order_status(self, status):
|
|
statuses: dict = {
|
|
'live': 'open',
|
|
'partially_filled': 'open',
|
|
'filled': 'closed',
|
|
'cancelled': 'canceled',
|
|
'not_trigger': 'open',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
watches trades made by the user
|
|
|
|
https://www.bitget.com/api-doc/contract/websocket/private/Fill-Channel
|
|
https://www.bitget.com/api-doc/uta/websocket/private/Fill-Channel
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
messageHash = 'myTrades'
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash = messageHash + ':' + symbol
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchMyTrades', market, params)
|
|
instType = None
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchMyTrades', 'uta', False)
|
|
if market is None and type == 'spot':
|
|
instType = 'SPOT'
|
|
else:
|
|
instType, params = self.get_inst_type(market, uta, params)
|
|
if uta:
|
|
instType = 'UTA'
|
|
subscriptionHash = 'fill:' + instType
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
args[topicOrChannel] = 'fill'
|
|
if not uta:
|
|
args['instId'] = 'default'
|
|
else:
|
|
params = self.extend(params, {'uta': True})
|
|
trades = await self.watch_private(messageHash, subscriptionHash, args, params)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trades(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "SPOT",
|
|
# "channel": "fill",
|
|
# "instId": "default"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "orderId": "1169142457356959747",
|
|
# "tradeId": "1169142457636958209",
|
|
# "symbol": "LTCUSDT",
|
|
# "orderType": "market",
|
|
# "side": "buy",
|
|
# "priceAvg": "81.069",
|
|
# "size": "0.074",
|
|
# "amount": "5.999106",
|
|
# "tradeScope": "taker",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "LTC",
|
|
# "deduction": "no",
|
|
# "totalDeductionFee": "0",
|
|
# "totalFee": "0.000074"
|
|
# }
|
|
# ],
|
|
# "cTime": "1714471204194",
|
|
# "uTime": "1714471204194"
|
|
# }
|
|
# ],
|
|
# "ts": 1714471204270
|
|
# }
|
|
# swap
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {
|
|
# "instType": "USDT-FUTURES",
|
|
# "channel": "fill",
|
|
# "instId": "default"
|
|
# },
|
|
# "data": [
|
|
# {
|
|
# "orderId": "1169142761031114781",
|
|
# "tradeId": "1169142761312637004",
|
|
# "symbol": "LTCUSDT",
|
|
# "orderType": "market",
|
|
# "side": "buy",
|
|
# "price": "80.87",
|
|
# "baseVolume": "0.1",
|
|
# "quoteVolume": "8.087",
|
|
# "profit": "0",
|
|
# "tradeSide": "open",
|
|
# "posMode": "hedge_mode",
|
|
# "tradeScope": "taker",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "deduction": "no",
|
|
# "totalDeductionFee": "0",
|
|
# "totalFee": "-0.0048522"
|
|
# }
|
|
# ],
|
|
# "cTime": "1714471276596",
|
|
# "uTime": "1714471276596"
|
|
# }
|
|
# ],
|
|
# "ts": 1714471276629
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "symbol": "BTCUSDT",
|
|
# "orderType": "market",
|
|
# "updatedTime": "1736378720623",
|
|
# "side": "buy",
|
|
# "orderId": "1288888888888888888",
|
|
# "execPnl": "0",
|
|
# "feeDetail": [
|
|
# {
|
|
# "feeCoin": "USDT",
|
|
# "fee": "0.569958"
|
|
# }
|
|
# ],
|
|
# "execTime": "1736378720623",
|
|
# "tradeScope": "taker",
|
|
# "tradeSide": "open",
|
|
# "execId": "1288888888888888888",
|
|
# "execLinkId": "1288888888888888888",
|
|
# "execPrice": "94993",
|
|
# "holdSide": "long",
|
|
# "execValue": "949.93",
|
|
# "category": "USDT-FUTURES",
|
|
# "execQty": "0.01",
|
|
# "clientOid": "1288888888888888889"
|
|
# }
|
|
# ],
|
|
# "arg": {
|
|
# "instType": "UTA",
|
|
# "topic": "fill"
|
|
# },
|
|
# "action": "snapshot",
|
|
# "ts": 1733904123981
|
|
# }
|
|
#
|
|
if self.myTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
self.myTrades = ArrayCache(limit)
|
|
stored = self.myTrades
|
|
data = self.safe_list(message, 'data', [])
|
|
length = len(data)
|
|
messageHash = 'myTrades'
|
|
for i in range(0, length):
|
|
trade = data[i]
|
|
parsed = self.parse_ws_trade(trade)
|
|
stored.append(parsed)
|
|
symbol = parsed['symbol']
|
|
symbolSpecificMessageHash = 'myTrades:' + symbol
|
|
client.resolve(stored, symbolSpecificMessageHash)
|
|
client.resolve(stored, messageHash)
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
watch balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://www.bitget.com/api-doc/spot/websocket/private/Account-Channel
|
|
https://www.bitget.com/api-doc/contract/websocket/private/Account-Channel
|
|
https://www.bitget.com/api-doc/margin/cross/websocket/private/Margin-Cross-Account-Assets
|
|
https://www.bitget.com/api-doc/margin/isolated/websocket/private/Margin-isolated-account-assets
|
|
https://www.bitget.com/api-doc/uta/websocket/private/Account-Channel
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.type]: spot or contract if not provided self.options['defaultType'] is used
|
|
:param str [params.instType]: one of 'SPOT', 'MARGIN', 'USDT-FUTURES', 'USDC-FUTURES', 'COIN-FUTURES', 'SUSDT-FUTURES', 'SUSDC-FUTURES' or 'SCOIN-FUTURES'
|
|
:param str [params.marginMode]: 'isolated' or 'cross' for watching spot margin balances
|
|
:param boolean [params.uta]: set to True for the unified trading account(uta), defaults to False
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
uta = None
|
|
uta, params = self.handle_option_and_params(params, 'watchBalance', 'uta', False)
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchBalance', None, params)
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('watchBalance', params)
|
|
instType = None
|
|
channel = 'account'
|
|
if (type == 'swap') or (type == 'future'):
|
|
instType = 'USDT-FUTURES'
|
|
elif marginMode is not None:
|
|
instType = 'MARGIN'
|
|
if not uta:
|
|
if marginMode == 'isolated':
|
|
channel = 'account-isolated'
|
|
else:
|
|
channel = 'account-crossed'
|
|
elif not uta:
|
|
instType = 'SPOT'
|
|
instType, params = self.handle_option_and_params(params, 'watchBalance', 'instType', instType)
|
|
if uta:
|
|
instType = 'UTA'
|
|
args: dict = {
|
|
'instType': instType,
|
|
}
|
|
topicOrChannel = 'topic' if uta else 'channel'
|
|
args[topicOrChannel] = channel
|
|
if not uta:
|
|
args['coin'] = 'default'
|
|
else:
|
|
params = self.extend(params, {'uta': True})
|
|
messageHash = 'balance:' + instType.lower()
|
|
return await self.watch_private(messageHash, messageHash, args, params)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "SPOT", "channel": "account", "coin": "default"},
|
|
# "data": [
|
|
# {
|
|
# "coin": "USDT",
|
|
# "available": "19.1430952856087",
|
|
# "frozen": "7",
|
|
# "locked": "0",
|
|
# "limitAvailable": "0",
|
|
# "uTime": "1701931970487"
|
|
# },
|
|
# ],
|
|
# "ts": 1701931970487
|
|
# }
|
|
#
|
|
# swap
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "USDT-FUTURES", "channel": "account", "coin": "default"},
|
|
# "data": [
|
|
# {
|
|
# "marginCoin": "USDT",
|
|
# "frozen": "5.36581500",
|
|
# "available": "26.14309528",
|
|
# "maxOpenPosAvailable": "20.77728028",
|
|
# "maxTransferOut": "20.77728028",
|
|
# "equity": "26.14309528",
|
|
# "usdtEquity": "26.143095285166"
|
|
# }
|
|
# ],
|
|
# "ts": 1701932570822
|
|
# }
|
|
#
|
|
# margin
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "MARGIN", "channel": "account-crossed", "coin": "default"},
|
|
# "data": [
|
|
# {
|
|
# "uTime": "1701933110544",
|
|
# "id": "1096916799926710272",
|
|
# "coin": "USDT",
|
|
# "available": "16.24309528",
|
|
# "borrow": "0.00000000",
|
|
# "frozen": "9.90000000",
|
|
# "interest": "0.00000000",
|
|
# "coupon": "0.00000000"
|
|
# }
|
|
# ],
|
|
# "ts": 1701933110544
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "data": [{
|
|
# "unrealisedPnL": "-10116.55",
|
|
# "totalEquity": "4976919.05",
|
|
# "positionMgnRatio": "0",
|
|
# "mmr": "408.08",
|
|
# "effEquity": "4847952.35",
|
|
# "imr": "17795.97",
|
|
# "mgnRatio": "0",
|
|
# "coin": [{
|
|
# "debts": "0",
|
|
# "balance": "0.9992",
|
|
# "available": "0.9992",
|
|
# "borrow": "0",
|
|
# "locked": "0",
|
|
# "equity": "0.9992",
|
|
# "coin": "ETH",
|
|
# "usdValue": "2488.667472"
|
|
# }]
|
|
# }],
|
|
# "arg": {
|
|
# "instType": "UTA",
|
|
# "topic": "account"
|
|
# },
|
|
# "action": "snapshot",
|
|
# "ts": 1740546523244
|
|
# }
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
data = self.safe_value(message, 'data', [])
|
|
for i in range(0, len(data)):
|
|
rawBalance = data[i]
|
|
if instType == 'uta':
|
|
coins = self.safe_list(rawBalance, 'coin', [])
|
|
for j in range(0, len(coins)):
|
|
entry = coins[j]
|
|
currencyId = self.safe_string(entry, 'coin')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.balance[code] if (code in self.balance) else self.account()
|
|
borrow = self.safe_string(entry, 'borrow')
|
|
debts = self.safe_string(entry, 'debts')
|
|
if (borrow is not None) or (debts is not None):
|
|
account['debt'] = Precise.string_add(borrow, debts)
|
|
account['free'] = self.safe_string(entry, 'available')
|
|
account['used'] = self.safe_string(entry, 'locked')
|
|
account['total'] = self.safe_string(entry, 'balance')
|
|
self.balance[code] = account
|
|
else:
|
|
currencyId = self.safe_string_2(rawBalance, 'coin', 'marginCoin')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.balance[code] if (code in self.balance) else self.account()
|
|
borrow = self.safe_string(rawBalance, 'borrow')
|
|
if borrow is not None:
|
|
interest = self.safe_string(rawBalance, 'interest')
|
|
account['debt'] = Precise.string_add(borrow, interest)
|
|
freeQuery = 'maxTransferOut' if ('maxTransferOut' in rawBalance) else 'available'
|
|
account['free'] = self.safe_string(rawBalance, freeQuery)
|
|
account['total'] = self.safe_string(rawBalance, 'equity')
|
|
account['used'] = self.safe_string(rawBalance, 'frozen')
|
|
self.balance[code] = account
|
|
self.balance = self.safe_balance(self.balance)
|
|
messageHash = 'balance:' + instType
|
|
client.resolve(self.balance, messageHash)
|
|
|
|
async def watch_public(self, messageHash, args, params={}):
|
|
uta = None
|
|
url = None
|
|
uta, params = self.handle_option_and_params(params, 'watchPublic', 'uta', False)
|
|
if uta:
|
|
url = self.urls['api']['ws']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['ws']['public']
|
|
sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False)
|
|
if sandboxMode:
|
|
instType = self.safe_string(args, 'instType')
|
|
if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'):
|
|
if uta:
|
|
url = self.urls['api']['demo']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['demo']['public']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [args],
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch(url, messageHash, message, messageHash)
|
|
|
|
async def un_watch_public(self, messageHash, args, params={}):
|
|
uta = None
|
|
url = None
|
|
uta, params = self.handle_option_and_params(params, 'unWatchPublic', 'uta', False)
|
|
if uta:
|
|
url = self.urls['api']['ws']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['ws']['public']
|
|
sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False)
|
|
if sandboxMode:
|
|
instType = self.safe_string(args, 'instType')
|
|
if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'):
|
|
if uta:
|
|
url = self.urls['api']['demo']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['demo']['public']
|
|
request: dict = {
|
|
'op': 'unsubscribe',
|
|
'args': [args],
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch(url, messageHash, message, messageHash)
|
|
|
|
async def watch_public_multiple(self, messageHashes, argsArray, params={}):
|
|
uta = None
|
|
url = None
|
|
uta, params = self.handle_option_and_params(params, 'watchPublicMultiple', 'uta', False)
|
|
if uta:
|
|
url = self.urls['api']['ws']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['ws']['public']
|
|
sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False)
|
|
if sandboxMode:
|
|
argsArrayFirst = self.safe_dict(argsArray, 0, {})
|
|
instType = self.safe_string(argsArrayFirst, 'instType')
|
|
if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'):
|
|
if uta:
|
|
url = self.urls['api']['demo']['utaPublic']
|
|
else:
|
|
url = self.urls['api']['demo']['public']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': argsArray,
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch_multiple(url, messageHashes, message, messageHashes)
|
|
|
|
async def authenticate(self, params={}):
|
|
self.check_required_credentials()
|
|
url = self.safe_string(params, 'url')
|
|
client = self.client(url)
|
|
messageHash = 'authenticated'
|
|
future = client.reusableFuture(messageHash)
|
|
authenticated = self.safe_value(client.subscriptions, messageHash)
|
|
if authenticated is None:
|
|
timestamp = str(self.seconds())
|
|
auth = timestamp + 'GET' + '/user/verify'
|
|
signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64')
|
|
operation = 'login'
|
|
request: dict = {
|
|
'op': operation,
|
|
'args': [
|
|
{
|
|
'apiKey': self.apiKey,
|
|
'passphrase': self.password,
|
|
'timestamp': timestamp,
|
|
'sign': signature,
|
|
},
|
|
],
|
|
}
|
|
message = self.extend(request, params)
|
|
self.watch(url, messageHash, message, messageHash)
|
|
return await future
|
|
|
|
async def watch_private(self, messageHash, subscriptionHash, args, params={}):
|
|
uta = None
|
|
url = None
|
|
uta, params = self.handle_option_and_params(params, 'watchPrivate', 'uta', False)
|
|
if uta:
|
|
url = self.urls['api']['ws']['utaPrivate']
|
|
else:
|
|
url = self.urls['api']['ws']['private']
|
|
sandboxMode = self.safe_bool_2(self.options, 'sandboxMode', 'sandbox', False)
|
|
if sandboxMode:
|
|
instType = self.safe_string(args, 'instType')
|
|
if (instType != 'SCOIN-FUTURES') and (instType != 'SUSDT-FUTURES') and (instType != 'SUSDC-FUTURES'):
|
|
if uta:
|
|
url = self.urls['api']['demo']['utaPrivate']
|
|
else:
|
|
url = self.urls['api']['demo']['private']
|
|
await self.authenticate({'url': url})
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [args],
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch(url, messageHash, message, subscriptionHash)
|
|
|
|
def handle_authenticate(self, client: Client, message):
|
|
#
|
|
# {event: "login", code: 0}
|
|
#
|
|
messageHash = 'authenticated'
|
|
future = self.safe_value(client.futures, messageHash)
|
|
future.resolve(True)
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# {event: "error", code: 30015, msg: "Invalid sign"}
|
|
#
|
|
event = self.safe_string(message, 'event')
|
|
try:
|
|
if event == 'error':
|
|
code = self.safe_string(message, 'code')
|
|
feedback = self.id + ' ' + self.json(message)
|
|
self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback)
|
|
msg = self.safe_string(message, 'msg', '')
|
|
self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], msg, feedback)
|
|
raise ExchangeError(feedback)
|
|
return False
|
|
except Exception as e:
|
|
if isinstance(e, AuthenticationError):
|
|
messageHash = 'authenticated'
|
|
client.reject(e, messageHash)
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
else:
|
|
# Note: if error happens on a subscribe event, user will have to close exchange to resubscribe. Issue #19041
|
|
client.reject(e)
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {instType: 'SPOT', channel: "ticker", instId: "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "instId": "BTCUSDT",
|
|
# "last": "21150.53",
|
|
# "open24h": "20759.65",
|
|
# "high24h": "21202.29",
|
|
# "low24h": "20518.82",
|
|
# "bestBid": "21150.500000",
|
|
# "bestAsk": "21150.600000",
|
|
# "baseVolume": "25402.1961",
|
|
# "quoteVolume": "530452554.2156",
|
|
# "ts": 1656408934044,
|
|
# "labeId": 0
|
|
# }
|
|
# ]
|
|
# }
|
|
# pong message
|
|
# "pong"
|
|
#
|
|
# login
|
|
#
|
|
# {event: "login", code: 0}
|
|
#
|
|
# subscribe
|
|
#
|
|
# {
|
|
# "event": "subscribe",
|
|
# "arg": {instType: 'SPOT', channel: "account", instId: "default"}
|
|
# }
|
|
# unsubscribe
|
|
# {
|
|
# "op":"unsubscribe",
|
|
# "args":[
|
|
# {
|
|
# "instType":"USDT-FUTURES",
|
|
# "channel":"ticker",
|
|
# "instId":"BTCUSDT"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# uta
|
|
#
|
|
# {
|
|
# "action": "snapshot",
|
|
# "arg": {"instType": "spot", topic: "ticker", symbol: "BTCUSDT"},
|
|
# "data": [
|
|
# {
|
|
# "highPrice24h": "120255.61",
|
|
# "lowPrice24h": "116145.88",
|
|
# "openPrice24h": "118919.38",
|
|
# "lastPrice": "119818.83",
|
|
# "turnover24h": "215859996.272276",
|
|
# "volume24h": "1819.756798",
|
|
# "bid1Price": "119811.26",
|
|
# "ask1Price": "119831.18",
|
|
# "bid1Size": "0.008732",
|
|
# "ask1Size": "0.004297",
|
|
# "price24hPcnt": "0.02002"
|
|
# }
|
|
# ],
|
|
# "ts": 1753230479687
|
|
# }
|
|
#
|
|
# unsubscribe
|
|
#
|
|
# {
|
|
# "event": "unsubscribe",
|
|
# "arg": {
|
|
# "instType": "spot",
|
|
# "topic": "kline",
|
|
# "symbol": "BTCUSDT",
|
|
# "interval": "1m"
|
|
# }
|
|
# }
|
|
#
|
|
if self.handle_error_message(client, message):
|
|
return
|
|
content = self.safe_string(message, 'message')
|
|
if content == 'pong':
|
|
self.handle_pong(client, message)
|
|
return
|
|
if message == 'pong':
|
|
self.handle_pong(client, message)
|
|
return
|
|
event = self.safe_string(message, 'event')
|
|
if event == 'login':
|
|
self.handle_authenticate(client, message)
|
|
return
|
|
if event == 'subscribe':
|
|
self.handle_subscription_status(client, message)
|
|
return
|
|
if event == 'unsubscribe':
|
|
self.handle_un_subscription_status(client, message)
|
|
return
|
|
methods: dict = {
|
|
'ticker': self.handle_ticker,
|
|
'trade': self.handle_trades,
|
|
'publicTrade': self.handle_trades,
|
|
'fill': self.handle_my_trades,
|
|
'order': self.handle_order,
|
|
'orders': self.handle_order,
|
|
'ordersAlgo': self.handle_order,
|
|
'orders-algo': self.handle_order,
|
|
'orders-crossed': self.handle_order,
|
|
'orders-isolated': self.handle_order,
|
|
'account': self.handle_balance,
|
|
'position': self.handle_positions,
|
|
'positions': self.handle_positions,
|
|
'account-isolated': self.handle_balance,
|
|
'account-crossed': self.handle_balance,
|
|
'kline': self.handle_ohlcv,
|
|
}
|
|
arg = self.safe_value(message, 'arg', {})
|
|
topic = self.safe_value_2(arg, 'channel', 'topic', '')
|
|
method = self.safe_value(methods, topic)
|
|
if method is not None:
|
|
method(client, message)
|
|
if topic.find('candle') >= 0:
|
|
self.handle_ohlcv(client, message)
|
|
if topic.find('books') >= 0:
|
|
self.handle_order_book(client, message)
|
|
|
|
def ping(self, client: Client):
|
|
return 'ping'
|
|
|
|
def handle_pong(self, client: Client, message):
|
|
client.lastPong = self.milliseconds()
|
|
return message
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "event": "subscribe",
|
|
# "arg": {instType: 'SPOT', channel: "account", instId: "default"}
|
|
# }
|
|
#
|
|
return message
|
|
|
|
def handle_order_book_un_subscription(self, client: Client, message):
|
|
#
|
|
# {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"books","instId":"BTCUSDT"}}
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
type = 'spot' if (instType == 'spot') else 'contract'
|
|
instId = self.safe_string(arg, 'instId')
|
|
market = self.safe_market(instId, None, None, type)
|
|
symbol = market['symbol']
|
|
messageHash = 'unsubscribe:orderbook:' + market['symbol']
|
|
subMessageHash = 'orderbook:' + symbol
|
|
if symbol in self.orderbooks:
|
|
del self.orderbooks[symbol]
|
|
if subMessageHash in client.subscriptions:
|
|
del client.subscriptions[subMessageHash]
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
error = UnsubscribeError(self.id + ' orderbook ' + symbol)
|
|
if subMessageHash in client.futures:
|
|
client.reject(error, subMessageHash)
|
|
client.resolve(True, messageHash)
|
|
|
|
def handle_trades_un_subscription(self, client: Client, message):
|
|
#
|
|
# {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"trade","instId":"BTCUSDT"}}
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
type = 'spot' if (instType == 'spot') else 'contract'
|
|
instId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
market = self.safe_market(instId, None, None, type)
|
|
symbol = market['symbol']
|
|
messageHash = 'unsubscribe:trade:' + market['symbol']
|
|
subMessageHash = 'trade:' + symbol
|
|
if symbol in self.trades:
|
|
del self.trades[symbol]
|
|
if subMessageHash in client.subscriptions:
|
|
del client.subscriptions[subMessageHash]
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
error = UnsubscribeError(self.id + ' trades ' + symbol)
|
|
if subMessageHash in client.futures:
|
|
client.reject(error, subMessageHash)
|
|
client.resolve(True, messageHash)
|
|
|
|
def handle_ticker_un_subscription(self, client: Client, message):
|
|
#
|
|
# {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"trade","instId":"BTCUSDT"}}
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
type = 'spot' if (instType == 'spot') else 'contract'
|
|
instId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
market = self.safe_market(instId, None, None, type)
|
|
symbol = market['symbol']
|
|
messageHash = 'unsubscribe:ticker:' + market['symbol']
|
|
subMessageHash = 'ticker:' + symbol
|
|
if symbol in self.tickers:
|
|
del self.tickers[symbol]
|
|
if subMessageHash in client.subscriptions:
|
|
del client.subscriptions[subMessageHash]
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
error = UnsubscribeError(self.id + ' ticker ' + symbol)
|
|
if subMessageHash in client.futures:
|
|
client.reject(error, subMessageHash)
|
|
client.resolve(True, messageHash)
|
|
|
|
def handle_ohlcv_un_subscription(self, client: Client, message):
|
|
#
|
|
# {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"candle1m","instId":"BTCUSDT"}}
|
|
#
|
|
# UTA
|
|
#
|
|
# {"event":"unsubscribe","arg":{"instType":"spot","topic":"kline","symbol":"BTCUSDT","interval":"1m"}}
|
|
#
|
|
arg = self.safe_dict(message, 'arg', {})
|
|
instType = self.safe_string_lower(arg, 'instType')
|
|
type = 'spot' if (instType == 'spot') else 'contract'
|
|
instId = self.safe_string_2(arg, 'instId', 'symbol')
|
|
channel = self.safe_string_2(arg, 'channel', 'topic')
|
|
interval = self.safe_string(arg, 'interval')
|
|
isUta = None
|
|
if interval is None:
|
|
isUta = False
|
|
interval = channel.replace('candle', '')
|
|
else:
|
|
isUta = True
|
|
timeframes = self.safe_value(self.options, 'timeframes')
|
|
timeframe = self.find_timeframe(interval, timeframes)
|
|
market = self.safe_market(instId, None, None, type)
|
|
symbol = market['symbol']
|
|
messageHash = None
|
|
subMessageHash = None
|
|
if isUta:
|
|
messageHash = 'unsubscribe:kline:' + symbol
|
|
subMessageHash = 'kline:' + symbol
|
|
else:
|
|
messageHash = 'unsubscribe:candles:' + timeframe + ':' + symbol
|
|
subMessageHash = 'candles:' + timeframe + ':' + symbol
|
|
if symbol in self.ohlcvs:
|
|
if timeframe in self.ohlcvs[symbol]:
|
|
del self.ohlcvs[symbol][timeframe]
|
|
self.clean_unsubscription(client, subMessageHash, messageHash)
|
|
|
|
def handle_un_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "op":"unsubscribe",
|
|
# "args":[
|
|
# {
|
|
# "instType":"USDT-FUTURES",
|
|
# "channel":"ticker",
|
|
# "instId":"BTCUSDT"
|
|
# },
|
|
# {
|
|
# "instType":"USDT-FUTURES",
|
|
# "channel":"candle1m",
|
|
# "instId":"BTCUSDT"
|
|
# }
|
|
# ]
|
|
# }
|
|
# or
|
|
# {"event":"unsubscribe","arg":{"instType":"SPOT","channel":"books","instId":"BTCUSDT"}}
|
|
#
|
|
argsList = self.safe_list(message, 'args')
|
|
if argsList is None:
|
|
argsList = [self.safe_dict(message, 'arg', {})]
|
|
for i in range(0, len(argsList)):
|
|
arg = argsList[i]
|
|
channel = self.safe_string_2(arg, 'channel', 'topic')
|
|
if channel == 'books':
|
|
# for now only unWatchOrderBook is supporteod
|
|
self.handle_order_book_un_subscription(client, message)
|
|
elif (channel == 'trade') or (channel == 'publicTrade'):
|
|
self.handle_trades_un_subscription(client, message)
|
|
elif channel == 'ticker':
|
|
self.handle_ticker_un_subscription(client, message)
|
|
elif channel.startswith('candle'):
|
|
self.handle_ohlcv_un_subscription(client, message)
|
|
elif channel.startswith('kline'):
|
|
self.handle_ohlcv_un_subscription(client, message)
|
|
return message
|