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

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