Initial commit: 首次建仓,建立目录结构
This commit is contained in:
730
dashboard/venv/lib/python3.12/site-packages/ccxt/pro/bullish.py
Normal file
730
dashboard/venv/lib/python3.12/site-packages/ccxt/pro/bullish.py
Normal file
@ -0,0 +1,730 @@
|
||||
# -*- 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
|
||||
from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Position, Str, Strings, Ticker, Trade
|
||||
from ccxt.async_support.base.ws.client import Client
|
||||
from typing import List
|
||||
from ccxt.base.errors import ExchangeError
|
||||
|
||||
|
||||
class bullish(ccxt.async_support.bullish):
|
||||
|
||||
def describe(self) -> Any:
|
||||
return self.deep_extend(super(bullish, self).describe(), {
|
||||
'has': {
|
||||
'ws': True,
|
||||
'watchTicker': True,
|
||||
'watchTickers': False,
|
||||
'watchOrderBook': True,
|
||||
'watchOrders': True,
|
||||
'watchTrades': True,
|
||||
'watchPositions': True,
|
||||
'watchMyTrades': True,
|
||||
'watchBalance': True,
|
||||
'watchOHLCV': False,
|
||||
},
|
||||
'urls': {
|
||||
'api': {
|
||||
'ws': {
|
||||
'public': 'wss://api.exchange.bullish.com',
|
||||
'private': 'wss://api.exchange.bullish.com/trading-api/v1/private-data',
|
||||
},
|
||||
},
|
||||
'test': {
|
||||
'ws': {
|
||||
'public': 'wss://api.simnext.bullish-test.com',
|
||||
'private': 'wss://api.simnext.bullish-test.com/trading-api/v1/private-data',
|
||||
},
|
||||
},
|
||||
},
|
||||
'options': {
|
||||
'ws': {
|
||||
'cookies': {},
|
||||
},
|
||||
},
|
||||
'streaming': {
|
||||
'ping': self.ping,
|
||||
'keepAlive': 99000, # disconnect after 100 seconds of inactivity
|
||||
},
|
||||
})
|
||||
|
||||
def request_id(self):
|
||||
requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1)
|
||||
self.options['requestId'] = requestId
|
||||
return requestId
|
||||
|
||||
def ping(self, client: Client):
|
||||
# bullish does not support built-in ws protocol-level ping-pong
|
||||
# https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--keep-websocket-open
|
||||
id = str(self.request_id())
|
||||
return {
|
||||
'jsonrpc': '2.0',
|
||||
'type': 'command',
|
||||
'method': 'keepalivePing',
|
||||
'params': {},
|
||||
'id': id,
|
||||
}
|
||||
|
||||
def handle_pong(self, client: Client, message):
|
||||
#
|
||||
# {
|
||||
# "id": "7",
|
||||
# "jsonrpc": "2.0",
|
||||
# "result": {
|
||||
# "responseCodeName": "OK",
|
||||
# "responseCode": "200",
|
||||
# "message": "Keep alive pong"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
client.lastPong = self.milliseconds()
|
||||
return message # current line is for transpilation compatibility
|
||||
|
||||
async def watch_public(self, url: str, messageHash: str, request={}, params={}) -> Any:
|
||||
id = str(self.request_id())
|
||||
message = {
|
||||
'jsonrpc': '2.0',
|
||||
'type': 'command',
|
||||
'method': 'subscribe',
|
||||
'params': request,
|
||||
'id': id,
|
||||
}
|
||||
fullUrl = self.urls['api']['ws']['public'] + url
|
||||
return await self.watch(fullUrl, messageHash, self.deep_extend(message, params), messageHash)
|
||||
|
||||
async def watch_private(self, messageHash: str, subscribeHash: str, request={}, params={}) -> Any:
|
||||
url = self.urls['api']['ws']['private']
|
||||
token = await self.handleToken()
|
||||
cookies = {
|
||||
'JWT_COOKIE': token,
|
||||
}
|
||||
self.options['ws']['cookies'] = cookies
|
||||
id = str(self.request_id())
|
||||
message = {
|
||||
'jsonrpc': '2.0',
|
||||
'type': 'command',
|
||||
'method': 'subscribe',
|
||||
'params': request,
|
||||
'id': id,
|
||||
}
|
||||
result = await self.watch(url, messageHash, self.deep_extend(message, params), subscribeHash)
|
||||
return result
|
||||
|
||||
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
||||
"""
|
||||
get the list of most recent trades for a particular symbol
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--unified-anonymous-trades-websocket-unauthenticated
|
||||
|
||||
:param str symbol: unified symbol of the market to fetch trades for
|
||||
:param int [since]: timestamp in ms of the earliest trade to fetch
|
||||
:param int [limit]: the maximum amount of trades to fetch
|
||||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||||
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
messageHash = 'trades::' + market['symbol']
|
||||
url = '/trading-api/v1/market-data/trades'
|
||||
request: Any = {
|
||||
'topic': 'anonymousTrades',
|
||||
'symbol': market['id'],
|
||||
}
|
||||
trades = await self.watch_public(url, messageHash, request, params)
|
||||
if self.newUpdates:
|
||||
limit = trades.getLimit(symbol, limit)
|
||||
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
||||
|
||||
def handle_trades(self, client: Client, message):
|
||||
#
|
||||
# {
|
||||
# "type": "snapshot",
|
||||
# "dataType": "V1TAAnonymousTradeUpdate",
|
||||
# "data": {
|
||||
# "trades": [
|
||||
# {
|
||||
# "tradeId": "100086000000609304",
|
||||
# "isTaker": True,
|
||||
# "price": "104889.2063",
|
||||
# "createdAtTimestamp": "1749124509118",
|
||||
# "quantity": "0.01000000",
|
||||
# "publishedAtTimestamp": "1749124531466",
|
||||
# "side": "BUY",
|
||||
# "createdAtDatetime": "2025-06-05T11:55:09.118Z",
|
||||
# "symbol": "BTCUSDC"
|
||||
# }
|
||||
# ],
|
||||
# "createdAtTimestamp": "1749124509118",
|
||||
# "publishedAtTimestamp": "1749124531466",
|
||||
# "symbol": "BTCUSDC"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
marketId = self.safe_string(data, 'symbol')
|
||||
symbol = self.safe_symbol(marketId)
|
||||
market = self.market(symbol)
|
||||
rawTrades = self.safe_list(data, 'trades', [])
|
||||
trades = self.parse_trades(rawTrades, market)
|
||||
if not (symbol in self.trades):
|
||||
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
||||
tradesArrayCache = ArrayCache(limit)
|
||||
self.trades[symbol] = tradesArrayCache
|
||||
tradesArray = self.trades[symbol]
|
||||
for i in range(0, len(trades)):
|
||||
tradesArray.append(trades[i])
|
||||
self.trades[symbol] = tradesArray
|
||||
messageHash = 'trades::' + market['symbol']
|
||||
client.resolve(tradesArray, messageHash)
|
||||
|
||||
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
||||
"""
|
||||
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--anonymous-market-data-price-tick-unauthenticated
|
||||
|
||||
:param str symbol: unified symbol of the market to fetch the ticker for
|
||||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||||
:returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
symbol = market['symbol']
|
||||
url = self.urls['api']['ws']['public'] + '/trading-api/v1/market-data/tick/' + market['id']
|
||||
messageHash = 'ticker::' + symbol
|
||||
return await self.watch(url, messageHash, params, messageHash) # no need to send a subscribe message, the server sends a ticker update on connect
|
||||
|
||||
def handle_ticker(self, client: Client, message):
|
||||
#
|
||||
# {
|
||||
# "type": "update",
|
||||
# "dataType": "V1TATickerResponse",
|
||||
# "data": {
|
||||
# "askVolume": "0.00100822",
|
||||
# "average": "104423.1806",
|
||||
# "baseVolume": "472.83799258",
|
||||
# "bestAsk": "104324.6000",
|
||||
# "bestBid": "104324.5000",
|
||||
# "bidVolume": "0.00020146",
|
||||
# "change": "-198.4864",
|
||||
# "close": "104323.9374",
|
||||
# "createdAtTimestamp": "1749132838951",
|
||||
# "publishedAtTimestamp": "1749132838955",
|
||||
# "high": "105966.6577",
|
||||
# "last": "104323.9374",
|
||||
# "lastTradeDatetime": "2025-06-05T14:13:56.111Z",
|
||||
# "lastTradeSize": "0.02396100",
|
||||
# "low": "104246.6662",
|
||||
# "open": "104522.4238",
|
||||
# "percentage": "-0.19",
|
||||
# "quoteVolume": "49662592.6712",
|
||||
# "symbol": "BTC-USDC-PERP",
|
||||
# "type": "ticker",
|
||||
# "vwap": "105030.6996",
|
||||
# "currentPrice": "104324.7747",
|
||||
# "ammData": [
|
||||
# {
|
||||
# "feeTierId": "1",
|
||||
# "currentPrice": "104324.7747",
|
||||
# "baseReservesQuantity": "8.27911366",
|
||||
# "quoteReservesQuantity": "1067283.0234",
|
||||
# "bidSpreadFee": "0.00000000",
|
||||
# "askSpreadFee": "0.00000000"
|
||||
# }
|
||||
# ],
|
||||
# "createdAtDatetime": "2025-06-05T14:13:58.951Z",
|
||||
# "markPrice": "104289.6884",
|
||||
# "fundingRate": "-0.000192",
|
||||
# "openInterest": "92.24146651"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
updateType = self.safe_string(message, 'type', '')
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
marketId = self.safe_string(data, 'symbol')
|
||||
market = self.safe_market(marketId)
|
||||
symbol = market['symbol']
|
||||
parsed = None
|
||||
if (updateType == 'snapshot'):
|
||||
parsed = self.parse_ticker(data, market)
|
||||
elif updateType == 'update':
|
||||
ticker = self.safe_dict(self.tickers, symbol, {})
|
||||
rawTicker = self.safe_dict(ticker, 'info', {})
|
||||
merged = self.extend(rawTicker, data)
|
||||
parsed = self.parse_ticker(merged, market)
|
||||
self.tickers[symbol] = parsed
|
||||
messageHash = 'ticker::' + symbol
|
||||
client.resolve(self.tickers[symbol], messageHash)
|
||||
|
||||
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
||||
"""
|
||||
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--multi-orderbook-websocket-unauthenticated
|
||||
|
||||
:param str symbol: unified symbol of the market to fetch the order book for
|
||||
:param int [limit]: the maximum amount of order book entries to return
|
||||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||||
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
|
||||
"""
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
url = '/trading-api/v1/market-data/orderbook'
|
||||
messageHash = 'orderbook::' + market['symbol']
|
||||
request: dict = {
|
||||
'topic': 'l2Orderbook', # 'l2Orderbook' returns only snapshots while 'l1Orderbook' returns only updates
|
||||
'symbol': market['id'],
|
||||
}
|
||||
orderbook = await self.watch_public(url, messageHash, request, params)
|
||||
return orderbook.limit()
|
||||
|
||||
def handle_order_book(self, client: Client, message):
|
||||
#
|
||||
# {
|
||||
# "type": "snapshot",
|
||||
# "dataType": "V1TALevel2",
|
||||
# "data": {
|
||||
# "timestamp": "1749372632028",
|
||||
# "bids": [
|
||||
# "105523.3000",
|
||||
# "0.00046045",
|
||||
# ],
|
||||
# "asks": [
|
||||
# "105523.4000",
|
||||
# "0.00117112",
|
||||
# ],
|
||||
# "publishedAtTimestamp": "1749372632073",
|
||||
# "datetime": "2025-06-08T08:50:32.028Z",
|
||||
# "sequenceNumberRange": [1967862061, 1967862062],
|
||||
# "symbol": "BTCUSDC"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# current channel is 'l2Orderbook' which returns only snapshots
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
marketId = self.safe_string(data, 'symbol')
|
||||
symbol = self.safe_symbol(marketId)
|
||||
messageHash = 'orderbook::' + symbol
|
||||
timestamp = self.safe_integer(data, 'timestamp')
|
||||
if not (symbol in self.orderbooks):
|
||||
self.orderbooks[symbol] = self.order_book()
|
||||
orderbook = self.orderbooks[symbol]
|
||||
bids = self.separate_bids_or_asks(self.safe_list(data, 'bids', []))
|
||||
asks = self.separate_bids_or_asks(self.safe_list(data, 'asks', []))
|
||||
snapshot = {
|
||||
'bids': bids,
|
||||
'asks': asks,
|
||||
}
|
||||
parsed = self.parse_order_book(snapshot, symbol, timestamp)
|
||||
sequenceNumberRange = self.safe_list(data, 'sequenceNumberRange', [])
|
||||
if len(sequenceNumberRange) > 0:
|
||||
lastIndex = len(sequenceNumberRange) - 1
|
||||
parsed['nonce'] = self.safe_integer(sequenceNumberRange, lastIndex)
|
||||
orderbook.reset(parsed)
|
||||
self.orderbooks[symbol] = orderbook
|
||||
client.resolve(orderbook, messageHash)
|
||||
|
||||
def separate_bids_or_asks(self, entry):
|
||||
result = []
|
||||
# 300 = '54885.0000000'
|
||||
# 301 = '0.06141566'
|
||||
# 302 ='53714.0000000'
|
||||
for i in range(0, len(entry)):
|
||||
if i % 2 != 0:
|
||||
continue
|
||||
price = self.safe_string(entry, i)
|
||||
amount = self.safe_string(entry, i + 1)
|
||||
result.append([price, amount])
|
||||
return result
|
||||
|
||||
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
||||
"""
|
||||
watches information on multiple orders made by the user
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--private-data-websocket-authenticated
|
||||
|
||||
: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 str [params.tradingAccountId]: the trading account id to fetch entries for
|
||||
:returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
subscribeHash = 'orders'
|
||||
messageHash = subscribeHash
|
||||
if symbol is not None:
|
||||
symbol = self.symbol(symbol)
|
||||
messageHash = messageHash + '::' + symbol
|
||||
request: dict = {
|
||||
'topic': 'orders',
|
||||
}
|
||||
tradingAccountId = self.safe_string(params, 'tradingAccountId')
|
||||
if tradingAccountId is not None:
|
||||
request['tradingAccountId'] = tradingAccountId
|
||||
params = self.omit(params, 'tradingAccountId')
|
||||
orders = await self.watch_private(messageHash, subscribeHash, request, params)
|
||||
if self.newUpdates:
|
||||
limit = orders.getLimit(symbol, limit)
|
||||
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
|
||||
|
||||
def handle_orders(self, client: Client, message):
|
||||
# snapshot
|
||||
# {
|
||||
# "type": "snapshot",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TAOrder",
|
||||
# "data": [...] # could be an empty list or a list of orders
|
||||
# }
|
||||
#
|
||||
# update
|
||||
# {
|
||||
# "type": "update",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TAOrder",
|
||||
# "data": {
|
||||
# "status": "OPEN",
|
||||
# "createdAtTimestamp": "1751893427971",
|
||||
# "quoteFee": "0.000000",
|
||||
# "stopPrice": null,
|
||||
# "quantityFilled": "0.00000000",
|
||||
# "handle": null,
|
||||
# "clientOrderId": null,
|
||||
# "quantity": "0.10000000",
|
||||
# "margin": False,
|
||||
# "side": "BUY",
|
||||
# "createdAtDatetime": "2025-07-07T13:03:47.971Z",
|
||||
# "isLiquidation": False,
|
||||
# "borrowedQuoteQuantity": null,
|
||||
# "borrowedBaseQuantity": null,
|
||||
# "timeInForce": "GTC",
|
||||
# "borrowedQuantity": null,
|
||||
# "baseFee": "0.000000",
|
||||
# "quoteAmount": "0.0000000",
|
||||
# "price": "0.0000000",
|
||||
# "statusReason": "Order accepted",
|
||||
# "type": "MKT",
|
||||
# "statusReasonCode": 6014,
|
||||
# "allowBorrow": False,
|
||||
# "orderId": "862317981870850049",
|
||||
# "publishedAtTimestamp": "1751893427975",
|
||||
# "symbol": "ETHUSDT",
|
||||
# "averageFillPrice": null
|
||||
# }
|
||||
# }
|
||||
#
|
||||
type = self.safe_string(message, 'type')
|
||||
rawOrders = []
|
||||
if type == 'update':
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
rawOrders.append(data) # update is a single order
|
||||
else:
|
||||
rawOrders = self.safe_list(message, 'data', []) # snapshot is a list of orders
|
||||
if len(rawOrders) > 0:
|
||||
if self.orders is None:
|
||||
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
||||
self.orders = ArrayCacheBySymbolById(limit)
|
||||
orders = self.orders
|
||||
symbols: dict = {}
|
||||
for i in range(0, len(rawOrders)):
|
||||
rawOrder = rawOrders[i]
|
||||
parsedOrder = self.parse_order(rawOrder)
|
||||
orders.append(parsedOrder)
|
||||
symbol = self.safe_string(parsedOrder, 'symbol')
|
||||
symbols[symbol] = True
|
||||
messageHash = 'orders'
|
||||
client.resolve(orders, messageHash)
|
||||
keys = list(symbols.keys())
|
||||
for i in range(0, len(keys)):
|
||||
hashSymbol = keys[i]
|
||||
symbolMessageHash = messageHash + '::' + hashSymbol
|
||||
client.resolve(self.orders, symbolMessageHash)
|
||||
|
||||
async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
||||
"""
|
||||
watches information on multiple trades made by the user
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--private-data-websocket-authenticated
|
||||
|
||||
:param str symbol: unified market symbol of the market trades were made in
|
||||
:param int [since]: the earliest time in ms to fetch trades for
|
||||
:param int [limit]: the maximum number of trade structures to retrieve
|
||||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||||
:param str [params.tradingAccountId]: the trading account id to fetch entries for
|
||||
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
subscribeHash = 'myTrades'
|
||||
messageHash = subscribeHash
|
||||
if symbol is not None:
|
||||
symbol = self.symbol(symbol)
|
||||
messageHash += '::' + symbol
|
||||
request: dict = {
|
||||
'topic': 'trades',
|
||||
}
|
||||
tradingAccountId = self.safe_string(params, 'tradingAccountId')
|
||||
if tradingAccountId is not None:
|
||||
request['tradingAccountId'] = tradingAccountId
|
||||
params = self.omit(params, 'tradingAccountId')
|
||||
trades = await self.watch_private(messageHash, subscribeHash, request, params)
|
||||
if self.newUpdates:
|
||||
limit = trades.getLimit(symbol, limit)
|
||||
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
||||
|
||||
def handle_my_trades(self, client: Client, message):
|
||||
#
|
||||
# snapshot
|
||||
# {
|
||||
# "type": "snapshot",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TATrade",
|
||||
# "data": [...] # could be an empty list or a list of trades
|
||||
# }
|
||||
#
|
||||
# update
|
||||
# {
|
||||
# "type": "update",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TATrade",
|
||||
# "data": {
|
||||
# "clientOtcTradeId": null,
|
||||
# "tradeId": "100203000003940164",
|
||||
# "baseFee": "0.00000000",
|
||||
# "isTaker": True,
|
||||
# "quoteAmount": "253.6012195",
|
||||
# "price": "2536.0121950",
|
||||
# "createdAtTimestamp": "1751914859840",
|
||||
# "quoteFee": "0.0000000",
|
||||
# "tradeRebateAmount": null,
|
||||
# "tradeRebateAssetSymbol": null,
|
||||
# "handle": null,
|
||||
# "otcTradeId": null,
|
||||
# "otcMatchId": null,
|
||||
# "orderId": "862407873644725249",
|
||||
# "quantity": "0.10000000",
|
||||
# "publishedAtTimestamp": "1751914859843",
|
||||
# "side": "SELL",
|
||||
# "createdAtDatetime": "2025-07-07T19:00:59.840Z",
|
||||
# "symbol": "ETHUSDT"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
type = self.safe_string(message, 'type')
|
||||
rawTrades = []
|
||||
if type == 'update':
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
rawTrades.append(data) # update is a single trade
|
||||
else:
|
||||
rawTrades = self.safe_list(message, 'data', []) # snapshot is a list of trades
|
||||
if len(rawTrades) > 0:
|
||||
if self.myTrades is None:
|
||||
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
||||
self.myTrades = ArrayCacheBySymbolById(limit)
|
||||
trades = self.myTrades
|
||||
symbols: dict = {}
|
||||
for i in range(0, len(rawTrades)):
|
||||
rawTrade = rawTrades[i]
|
||||
parsedTrade = self.parse_trade(rawTrade)
|
||||
trades.append(parsedTrade)
|
||||
symbol = self.safe_string(parsedTrade, 'symbol')
|
||||
symbols[symbol] = True
|
||||
messageHash = 'myTrades'
|
||||
client.resolve(trades, messageHash)
|
||||
keys = list(symbols.keys())
|
||||
for i in range(0, len(keys)):
|
||||
hashSymbol = keys[i]
|
||||
symbolMessageHash = messageHash + '::' + hashSymbol
|
||||
client.resolve(self.myTrades, symbolMessageHash)
|
||||
|
||||
async def watch_balance(self, params={}) -> Balances:
|
||||
"""
|
||||
watch balance and get the amount of funds available for trading or funds locked in orders
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--private-data-websocket-authenticated
|
||||
|
||||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||||
:param str [params.tradingAccountId]: the trading account id to fetch entries for
|
||||
:returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
request: dict = {
|
||||
'topic': 'assetAccounts',
|
||||
}
|
||||
messageHash = 'balance'
|
||||
tradingAccountId = self.safe_string(params, 'tradingAccountId')
|
||||
if tradingAccountId is not None:
|
||||
params = self.omit(params, 'tradingAccountId')
|
||||
request['tradingAccountId'] = tradingAccountId
|
||||
messageHash += '::' + tradingAccountId
|
||||
return await self.watch_private(messageHash, messageHash, request, params)
|
||||
|
||||
def handle_balance(self, client: Client, message):
|
||||
#
|
||||
# snapshot
|
||||
# {
|
||||
# "type": "snapshot",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TAAssetAccount",
|
||||
# "data": [
|
||||
# {
|
||||
# "updatedAtTimestamp": "1751989627509",
|
||||
# "borrowedQuantity": "0.0000",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "loanedQuantity": "0.0000",
|
||||
# "lockedQuantity": "0.0000",
|
||||
# "assetId": "5",
|
||||
# "assetSymbol": "USDC",
|
||||
# "publishedAtTimestamp": "1751989627512",
|
||||
# "availableQuantity": "999672939.8767",
|
||||
# "updatedAtDatetime": "2025-07-08T15:47:07.509Z"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# update
|
||||
# {
|
||||
# "type": "update",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "dataType": "V1TAAssetAccount",
|
||||
# "data": {
|
||||
# "updatedAtTimestamp": "1751989627509",
|
||||
# "borrowedQuantity": "0.0000",
|
||||
# "tradingAccountId": "111309424211255",
|
||||
# "loanedQuantity": "0.0000",
|
||||
# "lockedQuantity": "0.0000",
|
||||
# "assetId": "5",
|
||||
# "assetSymbol": "USDC",
|
||||
# "publishedAtTimestamp": "1751989627512",
|
||||
# "availableQuantity": "999672939.8767",
|
||||
# "updatedAtDatetime": "2025-07-08T15:47:07.509Z"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
tradingAccountId = self.safe_string(message, 'tradingAccountId')
|
||||
if not (tradingAccountId in self.balance):
|
||||
self.balance[tradingAccountId] = {}
|
||||
messageType = self.safe_string(message, 'type')
|
||||
if messageType == 'snapshot':
|
||||
data = self.safe_list(message, 'data', [])
|
||||
self.balance[tradingAccountId] = self.parse_balance(data)
|
||||
else:
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
assetId = self.safe_string(data, 'assetSymbol')
|
||||
account = self.account()
|
||||
account['total'] = self.safe_string(data, 'availableQuantity')
|
||||
account['used'] = self.safe_string(data, 'lockedQuantity')
|
||||
code = self.safe_currency_code(assetId)
|
||||
self.balance[tradingAccountId][code] = account
|
||||
self.balance[tradingAccountId]['info'] = message
|
||||
self.balance[tradingAccountId] = self.safe_balance(self.balance[tradingAccountId])
|
||||
messageHash = 'balance'
|
||||
tradingAccountIdHash = '::' + tradingAccountId
|
||||
client.resolve(self.balance[tradingAccountId], messageHash)
|
||||
client.resolve(self.balance[tradingAccountId], messageHash + tradingAccountIdHash)
|
||||
|
||||
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
||||
"""
|
||||
|
||||
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--private-data-websocket-authenticated
|
||||
|
||||
watch all open positions
|
||||
:param str[] [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
|
||||
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
||||
"""
|
||||
await self.load_markets()
|
||||
subscribeHash = 'positions'
|
||||
messageHash = subscribeHash
|
||||
if not self.is_empty(symbols):
|
||||
symbols = self.market_symbols(symbols)
|
||||
messageHash += '::' + ','.join(symbols)
|
||||
request: dict = {
|
||||
'topic': 'derivativesPositionsV2',
|
||||
}
|
||||
positions = await self.watch_private(messageHash, subscribeHash, request, params)
|
||||
if self.newUpdates:
|
||||
return positions
|
||||
return self.filter_by_symbols_since_limit(positions, symbols, since, limit, True)
|
||||
|
||||
def handle_positions(self, client: Client, message):
|
||||
# exchange does not return messages for sandbox mode
|
||||
# current method is implemented blindly
|
||||
# todo: check if self works with not-sandbox mode
|
||||
messageType = self.safe_string(message, 'type')
|
||||
rawPositions = []
|
||||
if messageType == 'update':
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
rawPositions.append(data)
|
||||
else:
|
||||
rawPositions = self.safe_list(message, 'data', [])
|
||||
if self.positions is None:
|
||||
self.positions = ArrayCacheBySymbolBySide()
|
||||
positions = self.positions
|
||||
newPositions = []
|
||||
for i in range(0, len(rawPositions)):
|
||||
rawPosition = rawPositions[i]
|
||||
position = self.parse_position(rawPosition)
|
||||
positions.append(position)
|
||||
newPositions.append(position)
|
||||
messageHashes = self.find_message_hashes(client, 'positions::')
|
||||
for i in range(0, len(messageHashes)):
|
||||
messageHash = messageHashes[i]
|
||||
parts = messageHash.split('::')
|
||||
symbolsString = parts[1]
|
||||
symbols = symbolsString.split(',')
|
||||
symbolPositions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
||||
if not self.is_empty(symbolPositions):
|
||||
client.resolve(symbolPositions, messageHash)
|
||||
client.resolve(positions, 'positions')
|
||||
|
||||
def handle_error_message(self, client: Client, message):
|
||||
#
|
||||
# {
|
||||
# "data": {
|
||||
# "errorCode": 401,
|
||||
# "errorCodeName": "UNAUTHORIZED",
|
||||
# "message": "Unable to authenticate; JWT is missing/invalid or unauthorised to access account"
|
||||
# },
|
||||
# "dataType": "V1TAErrorResponse",
|
||||
# "type": "error"
|
||||
# }
|
||||
#
|
||||
data = self.safe_dict(message, 'data', {})
|
||||
feedback = self.id + ' ' + self.json(data)
|
||||
try:
|
||||
errorCode = self.safe_string(data, 'errorCode')
|
||||
errorCodeName = self.safe_string(data, 'errorCodeName')
|
||||
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
||||
self.throw_broadly_matched_exception(self.exceptions['broad'], errorCodeName, feedback)
|
||||
raise ExchangeError(feedback) # unknown message
|
||||
except Exception as e:
|
||||
client.reject(e)
|
||||
|
||||
def handle_message(self, client: Client, message):
|
||||
dataType = self.safe_string(message, 'dataType')
|
||||
result = self.safe_dict(message, 'result')
|
||||
if result is not None:
|
||||
response = self.safe_string(result, 'message')
|
||||
if response == 'Keep alive pong':
|
||||
self.handle_pong(client, message)
|
||||
elif dataType is not None:
|
||||
if dataType == 'V1TAAnonymousTradeUpdate':
|
||||
self.handle_trades(client, message)
|
||||
if dataType == 'V1TATickerResponse':
|
||||
self.handle_ticker(client, message)
|
||||
if dataType == 'V1TALevel2':
|
||||
self.handle_order_book(client, message)
|
||||
if dataType == 'V1TAOrder':
|
||||
self.handle_orders(client, message)
|
||||
if dataType == 'V1TATrade':
|
||||
self.handle_my_trades(client, message)
|
||||
if dataType == 'V1TAAssetAccount':
|
||||
self.handle_balance(client, message)
|
||||
if dataType == 'V1TAErrorResponse':
|
||||
self.handle_error_message(client, message)
|
||||
Reference in New Issue
Block a user