2932 lines
132 KiB
Python
2932 lines
132 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
|
|
|
|
from ccxt.base.exchange import Exchange
|
|
from ccxt.abstract.bullish import ImplicitAPI
|
|
import hashlib
|
|
from ccxt.base.types import Account, Any, Balances, Bool, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, OpenInterest, Trade, Transaction, FundingRateHistory, TransferEntry
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import PermissionDenied
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
from ccxt.base.errors import OperationRejected
|
|
from ccxt.base.errors import MarketClosed
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidAddress
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import OrderNotFillable
|
|
from ccxt.base.errors import DuplicateOrderId
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.errors import InvalidNonce
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
|
|
|
|
class bullish(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bullish, self).describe(), {
|
|
'id': 'bullish',
|
|
'name': 'Bullish',
|
|
'countries': ['DE'],
|
|
'version': 'v3',
|
|
'rateLimit': 20, # 50 requests per second
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'borrowMargin': False,
|
|
'cancelAllOrders': True,
|
|
'cancelOrder': True,
|
|
'cancelOrders': False,
|
|
'createDepositAddress': False,
|
|
'createLimitBuyOrder': True,
|
|
'createLimitOrder': True,
|
|
'createLimitSellOrder': True,
|
|
'createMarketBuyOrder': True,
|
|
'createMarketOrder': True,
|
|
'createMarketSellOrder': True,
|
|
'createOrder': True,
|
|
'createPostOnlyOrder': True,
|
|
'createTriggerOrder': True,
|
|
'deposit': False,
|
|
'editOrder': True,
|
|
'fetchAccounts': True,
|
|
'fetchBalance': True,
|
|
'fetchBidsAsks': False,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': True,
|
|
'fetchCanceledAndClosedOrders': True,
|
|
'fetchCanceledOrders': True,
|
|
'fetchClosedOrder': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': True,
|
|
'fetchDeposit': False,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDepositAddressesByNetwork': False,
|
|
'fetchDeposits': False,
|
|
'fetchDepositsWithdrawals': True,
|
|
'fetchDepositWithdrawFee': False,
|
|
'fetchDepositWithdrawFees': False,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': True,
|
|
'fetchFundingRates': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchL3OrderBook': False,
|
|
'fetchLedger': False,
|
|
'fetchLeverage': False,
|
|
'fetchLeverageTiers': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterest': True,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenInterests': False,
|
|
'fetchOpenOrder': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrderBooks': False,
|
|
'fetchOrders': True,
|
|
'fetchOrderTrades': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': True,
|
|
'fetchPositionsForSymbol': False,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchStatus': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': False,
|
|
'fetchTime': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': False,
|
|
'fetchTradingFees': False,
|
|
'fetchTradingLimits': False,
|
|
'fetchTransactionFee': False,
|
|
'fetchTransactionFees': False,
|
|
'fetchTransactions': False,
|
|
'fetchTransfers': True,
|
|
'fetchWithdrawal': False,
|
|
'fetchWithdrawals': False,
|
|
'fetchWithdrawalWhitelist': False,
|
|
'reduceMargin': False,
|
|
'repayMargin': False,
|
|
'setLeverage': False,
|
|
'setMargin': False,
|
|
'setMarginMode': False,
|
|
'setPositionMode': False,
|
|
'signIn': True,
|
|
'transfer': True,
|
|
'withdraw': True,
|
|
'ws': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'30m': '30m',
|
|
'1h': '1h',
|
|
'6h': '6h',
|
|
'12h': '12h',
|
|
'1d': '1d',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://github.com/user-attachments/assets/68f0686b-84f0-4da9-a751-f7089af3a9ed',
|
|
'api': {
|
|
'public': 'https://api.exchange.bullish.com/trading-api',
|
|
'private': 'https://api.exchange.bullish.com/trading-api',
|
|
},
|
|
'test': {
|
|
'public': 'https://api.simnext.bullish-test.com/trading-api',
|
|
'private': 'https://api.simnext.bullish-test.com/trading-api',
|
|
},
|
|
'www': 'https://bullish.com/',
|
|
'referral': '',
|
|
'doc': [
|
|
'https://api.exchange.bullish.com/docs/api/rest/',
|
|
],
|
|
},
|
|
'api': {
|
|
'public': {
|
|
'get': {
|
|
'v1/nonce': 1,
|
|
'v1/time': 1,
|
|
'v1/assets': 1,
|
|
'v1/assets/{symbol}': 1,
|
|
'v1/markets': 1,
|
|
'v1/markets/{symbol}': 1,
|
|
'v1/history/markets/{symbol}': 1,
|
|
'v1/markets/{symbol}/orderbook/hybrid': 1,
|
|
'v1/markets/{symbol}/trades': 1,
|
|
'v1/markets/{symbol}/tick': 1,
|
|
'v1/markets/{symbol}/candle': 1,
|
|
'v1/history/markets/{symbol}/trades': 1,
|
|
'v1/history/markets/{symbol}/funding-rate': 1,
|
|
'v1/index-prices': 1,
|
|
'v1/index-prices/{assetSymbol}': 1,
|
|
'v1/expiry-prices/{symbol}': 1,
|
|
'v1/option-ladder': 1,
|
|
'v1/option-ladder/{symbol}': 1,
|
|
},
|
|
},
|
|
'private': {
|
|
'get': {
|
|
'v2/orders': 1,
|
|
'v2/history/orders': 1,
|
|
'v2/orders/{orderId}': 1,
|
|
'v2/amm-instructions': 1,
|
|
'v2/amm-instructions/{instructionId}': 1,
|
|
'v1/wallets/transactions': 1,
|
|
'v1/wallets/limits/{symbol}': 1,
|
|
'v1/wallets/deposit-instructions/crypto/{symbol}': 1,
|
|
'v1/wallets/withdrawal-instructions/crypto/{symbol}': 1,
|
|
'v1/wallets/deposit-instructions/fiat/{symbol}': 1,
|
|
'v1/wallets/withdrawal-instructions/fiat/{symbol}': 1,
|
|
'v1/wallets/self-hosted/verification-attempts': 1,
|
|
'v1/trades': 5,
|
|
'v1/history/trades': 5,
|
|
'v1/trades/{tradeId}': 5,
|
|
'v1/trades/client-order-id/{clientOrderId}': 1,
|
|
'v1/accounts/asset': 1,
|
|
'v1/accounts/asset/{symbol}': 1,
|
|
'v1/users/logout': 1,
|
|
'v1/users/hmac/login': 1,
|
|
'v1/accounts/trading-accounts': 1,
|
|
'v1/accounts/trading-accounts/{tradingAccountId}': 1,
|
|
'v1/derivatives-positions': 1,
|
|
'v1/history/derivatives-settlement': 1,
|
|
'v1/history/transfer': 1,
|
|
'v1/history/borrow-interest': 1,
|
|
'v2/mmp-configuration': 1,
|
|
'v2/otc-trades': 1,
|
|
'v2/otc-trades/{otcTradeId}': 1,
|
|
'v2/otc-trades/unconfirmed-trade': 1,
|
|
},
|
|
'post': {
|
|
'v2/orders': 5,
|
|
'v2/command': 5,
|
|
'v2/amm-instructions': 1,
|
|
'v1/wallets/withdrawal': 1,
|
|
'v2/users/login': 1,
|
|
'v1/simulate-portfolio-margin': 1,
|
|
'v1/wallets/self-hosted/initiate': 1,
|
|
'v2/mmp-configuration': 1,
|
|
'v2/otc-trades': 1,
|
|
'v2/otc-command': 1,
|
|
},
|
|
},
|
|
},
|
|
'fees': {
|
|
'trading': {
|
|
'tierBased': False,
|
|
'percentage': True,
|
|
# todo check fees
|
|
'taker': self.parse_number('0.001'),
|
|
'maker': self.parse_number('0.001'),
|
|
},
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
# exchange-specific options
|
|
'options': {
|
|
'timeDifference': 0, # the difference between system clock and Binance clock
|
|
'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation
|
|
'networks': {
|
|
'BTC': 'BTC',
|
|
'EOS': 'EOS',
|
|
'ERC20': 'ETH',
|
|
},
|
|
'defaultNetwork': 'ERC20',
|
|
'defaultNetworks': {
|
|
'USDC': 'ERC20',
|
|
},
|
|
'tradingAccountId': None,
|
|
},
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': None,
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False,
|
|
'takeProfitPrice': False,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'hedged': False,
|
|
'trailing': False,
|
|
'leverage': False,
|
|
'marketBuyByCost': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'selfTradePrevention': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'symbolRequired': False,
|
|
'untilDays': 90,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'untilDays': 90,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'untilDays': 90,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchCanceledAndClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'untilDays': 90,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 1,
|
|
'daysBackCanceled': 1,
|
|
'untilDays': 1,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchCanceledOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 1,
|
|
'untilDays': 1,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 1000,
|
|
},
|
|
},
|
|
'spot': {
|
|
'extends': 'default',
|
|
},
|
|
'swap': {
|
|
'linear': {
|
|
'extends': 'default',
|
|
},
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': {
|
|
'extends': 'default',
|
|
},
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'exceptions': {
|
|
'exact': {
|
|
'1': BadRequest, # Unknown symbol
|
|
'5': InvalidOrder, # Unknown order
|
|
'6': DuplicateOrderId, # Duplicate order
|
|
'13': BadRequest, # Incorrect quantity
|
|
'15': BadRequest, # Invalid account
|
|
'18': BadRequest, # Invalid price
|
|
'1002': BadRequest, # Unable to place request
|
|
'2001': BadRequest, # Bad incoming request
|
|
'2002': BadRequest, # Invalid user's client id
|
|
'2003': BadRequest, # Invalid handle
|
|
'2004': BadRequest, # Invalid quantity
|
|
'2005': ExchangeError, # Unknown error
|
|
'2006': BadRequest, # Invalid account type, # account must be spot
|
|
'2007': BadRequest, # Account already exist
|
|
'2008': BadRequest, # Invalid side, # side must me from buy or sell
|
|
'2009': BadSymbol, # Invalid market
|
|
'2010': AuthenticationError, # Account doesn't exist
|
|
'2011': AuthenticationError, # Account types are different
|
|
'2012': BadRequest, # Invalid price
|
|
'2013': InvalidOrder, # Invalid order type, # type must be from limit, # market, # stop-limit
|
|
'2015': OperationRejected, # Exceeded maximum amount of allowed open margin orders
|
|
'2016': BadRequest, # Unknown request type
|
|
'2017': BadRequest, # Invalid order id
|
|
'2018': BadRequest, # Unknown time in force option
|
|
'2020': PermissionDenied, # Margin trading is not allowed
|
|
'2021': OperationRejected, # Exceeded maximum amount of allowed open spot orders
|
|
'2029': InvalidNonce, # Invalid request id
|
|
'2035': InvalidNonce, # Invalid nonce
|
|
'3001': InsufficientFunds, # Account doesn't have sufficient balance
|
|
'3002': OrderNotFound, # Order is not found
|
|
'3003': PermissionDenied, # Borrowing is unavailable
|
|
'3004': InsufficientFunds, # Unable to adjust balance
|
|
'3005': InsufficientFunds, # Insufficient balance
|
|
'3006': InsufficientFunds, # Insufficient collateral
|
|
'3007': DuplicateOrderId, # Duplicated order id
|
|
'3031': BadRequest, # Price is out of range
|
|
'3032': BadRequest, # Order is either closed or rejected
|
|
'3033': PermissionDenied, # Leverage increase not permitted
|
|
'3034': RateLimitExceeded, # Rate limit exceeded
|
|
'3035': RateLimitExceeded, # Global rate limit exceeded
|
|
'3047': OperationRejected, # Leverage increase not permitted
|
|
'3048': OperationRejected, # Reached max borrowing
|
|
'3049': OperationRejected, # No more open loans available
|
|
'3051': InsufficientFunds, # Insufficient iou balance
|
|
'3052': InsufficientFunds, # Insufficient uoi balance
|
|
'3063': BadRequest, # Missing request id
|
|
'3064': OrderNotFillable, # Incoming order failed to make or take
|
|
'3065': MarketClosed, # Market open interest limit exceeded
|
|
'3066': ExchangeError, # Account concentration limit exceeded
|
|
'3067': MarketClosed, # MarketClosed
|
|
'6007': InvalidOrder, # Self cross prevention
|
|
'6011': InvalidOrder, # Self cross prevention amend
|
|
'6012': InvalidOrder, # Stop limit amend
|
|
'6013': InvalidOrder, # Partially filled
|
|
'8301': ExchangeError, # Unexpected Error
|
|
'8305': ExchangeError, # Withdraw assertion failed
|
|
'8306': ExchangeError, # Custody bad user
|
|
'8307': ExchangeError, # Unexpected withdraw exception
|
|
'8310': InvalidAddress, # Cannot find withdrawal destination
|
|
'8311': BadRequest, # Missing fields in withdraw
|
|
'8313': BadRequest, # Unsupported coin
|
|
'8315': OperationRejected, # Crypto deposit not found
|
|
'8316': OperationRejected, # Unable to allocate deposit address
|
|
'8317': OperationRejected, # Swift code is on the restricted list
|
|
'8318': NotSupported, # Unsupported operation
|
|
'8319': NotSupported, # Custody operation has been disabled
|
|
'8320': InvalidAddress, # Address failed validation
|
|
'8322': BadRequest, # Bad withdrawal amount
|
|
'8327': AuthenticationError, # Invalid Login
|
|
'8329': ExchangeError, # Unexpected destination exception
|
|
'8331': InvalidAddress, # Invalid Destination
|
|
'8332': BadRequest, # Bad network specified
|
|
'8333': BadRequest, # Bad symbol specified
|
|
'8334': BadRequest, # Bad authentication type
|
|
'8335': InvalidAddress, # Withdrawal destination does not belong to user
|
|
'8336': InvalidAddress, # Withdrawal destination not whitelisted
|
|
'8399': ExchangeError, # Unknown error
|
|
},
|
|
'broad': {
|
|
'HttpInvalidParameterException': BadRequest,
|
|
'UNAUTHORIZED_COMMAND': AuthenticationError, # {"message":"Unauthorized to execute command","raw":null,"errorCode":6105,"errorCodeName":"UNAUTHORIZED_COMMAND"}
|
|
'QUERY_FILTER_ERROR': BadRequest, # {"message":"Field 'settlementDatetime' cannot be filtered","errorCode":23001,"errorCodeName":"QUERY_FILTER_ERROR"}
|
|
'INVALID_SYMBOL': BadSymbol, # {"message":"Invalid symbol provided","errorCode":28004,"errorCodeName":"INVALID_SYMBOL"}
|
|
},
|
|
},
|
|
})
|
|
|
|
def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--time
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
response = self.publicGetV1Time(params)
|
|
#
|
|
# {
|
|
# "datetime": "2025-05-05T20:05:50.999Z",
|
|
# "timestamp": 1746475550999
|
|
# }
|
|
#
|
|
return self.safe_integer(response, 'timestamp')
|
|
|
|
def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/assets
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
response = self.publicGetV1Assets(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "assetId": "72",
|
|
# "symbol": "BTT1M",
|
|
# "name": "BitTorrent(millions)",
|
|
# "precision": "5",
|
|
# "minBalanceInterest": "0.00000",
|
|
# "apr": "10.00",
|
|
# "minFee": "0.00000",
|
|
# "maxBorrow": "0.00000",
|
|
# "totalOfferedLoanQuantity": "0.00000",
|
|
# "loanBorrowedQuantity": "0.00000",
|
|
# "collateralBands":
|
|
# [
|
|
# {
|
|
# "collateralPercentage": "90.00",
|
|
# "bandLimitUSD": "100000.0000"
|
|
# },
|
|
# {
|
|
# "collateralPercentage": "68.00",
|
|
# "bandLimitUSD": "300000.0000"
|
|
# },
|
|
# {
|
|
# "collateralPercentage": "25.00",
|
|
# "bandLimitUSD": "600000.0000"
|
|
# }
|
|
# ],
|
|
# "underlyingAsset":
|
|
# {
|
|
# "symbol": "BTT1M",
|
|
# "assetId": "72",
|
|
# "bpmMinReturnStart": "0.9200",
|
|
# "bpmMinReturnEnd": "0.9300",
|
|
# "bpmMaxReturnStart": "1.0800",
|
|
# "bpmMaxReturnEnd": "1.0800",
|
|
# "marketRiskFloorPctStart": "2.60",
|
|
# "marketRiskFloorPctEnd": "2.50",
|
|
# "bpmTransitionDateTimeStart": "2025-05-05T08:00:00.000Z",
|
|
# "bpmTransitionDateTimeEnd": "2025-05-08T08:00:00.000Z"
|
|
# }
|
|
# }, ...
|
|
# ]
|
|
#
|
|
return self.parse_currencies(response)
|
|
|
|
def parse_currency(self, rawCurrency: dict) -> Currency:
|
|
id = self.safe_string(rawCurrency, 'symbol')
|
|
code = self.safe_currency_code(id)
|
|
name = self.safe_string(rawCurrency, 'name')
|
|
precision = self.safe_string(rawCurrency, 'precision')
|
|
return self.safe_currency_structure({
|
|
'id': id,
|
|
'code': code,
|
|
'name': name,
|
|
'active': None,
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': self.safe_number(rawCurrency, 'minFee'),
|
|
'precision': self.parse_number(self.parse_precision(precision)),
|
|
'limits': {
|
|
'amount': {'min': None, 'max': None},
|
|
'withdraw': {'min': None, 'max': None},
|
|
},
|
|
'networks': {},
|
|
'type': 'crypto',
|
|
'info': rawCurrency,
|
|
})
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for ace
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/markets
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
if self.options['adjustForTimeDifference']:
|
|
self.load_time_difference()
|
|
response = self.publicGetV1Markets(params)
|
|
return self.parse_markets(response)
|
|
|
|
def parse_market(self, market: dict) -> Market:
|
|
#
|
|
# {
|
|
# "marketId": "20069",
|
|
# "symbol": "BTC-USDC-20250516",
|
|
# "quoteAssetId": "5",
|
|
# "baseAssetId": "1",
|
|
# "quoteSymbol": "USDC",
|
|
# "baseSymbol": "BTC",
|
|
# "quotePrecision": "4",
|
|
# "basePrecision": "8",
|
|
# "pricePrecision": "4",
|
|
# "quantityPrecision": "8",
|
|
# "costPrecision": "4",
|
|
# "minQuantityLimit": "0.00050000",
|
|
# "maxQuantityLimit": "200.00000000",
|
|
# "maxPriceLimit": null,
|
|
# "minPriceLimit": null,
|
|
# "maxCostLimit": null,
|
|
# "minCostLimit": null,
|
|
# "timeZone": "Etc/UTC",
|
|
# "tickSize": "0.1000",
|
|
# "liquidityTickSize": "100.0000",
|
|
# "liquidityPrecision": "4",
|
|
# "makerFee": "0",
|
|
# "takerFee": "2",
|
|
# "roundingCorrectionFactor": "0.00000100",
|
|
# "makerMinLiquidityAddition": "1000000",
|
|
# "orderTypes":
|
|
# [
|
|
# "LMT",
|
|
# "MKT",
|
|
# "STOP_LIMIT",
|
|
# "POST_ONLY"
|
|
# ],
|
|
# "spotTradingEnabled": True,
|
|
# "marginTradingEnabled": True,
|
|
# "marketEnabled": True,
|
|
# "createOrderEnabled": True,
|
|
# "cancelOrderEnabled": True,
|
|
# "liquidityInvestEnabled": True,
|
|
# "liquidityWithdrawEnabled": True,
|
|
# "feeTiers":
|
|
# [
|
|
# {
|
|
# "feeTierId": "1",
|
|
# "staticSpreadFee": "0.00000000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "10",
|
|
# "staticSpreadFee": "0.00100000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "11",
|
|
# "staticSpreadFee": "0.00150000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "12",
|
|
# "staticSpreadFee": "0.00150000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "13",
|
|
# "staticSpreadFee": "0.00300000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "14",
|
|
# "staticSpreadFee": "0.00300000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "15",
|
|
# "staticSpreadFee": "0.00500000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "16",
|
|
# "staticSpreadFee": "0.00500000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "17",
|
|
# "staticSpreadFee": "0.01000000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "18",
|
|
# "staticSpreadFee": "0.01000000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "19",
|
|
# "staticSpreadFee": "0.01500000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "2",
|
|
# "staticSpreadFee": "0.00000000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "20",
|
|
# "staticSpreadFee": "0.01500000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "21",
|
|
# "staticSpreadFee": "0.02000000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "22",
|
|
# "staticSpreadFee": "0.02000000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "3",
|
|
# "staticSpreadFee": "0.00010000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "4",
|
|
# "staticSpreadFee": "0.00010000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "5",
|
|
# "staticSpreadFee": "0.00020000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "6",
|
|
# "staticSpreadFee": "0.00020000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "7",
|
|
# "staticSpreadFee": "0.00060000",
|
|
# "isDislocationEnabled": False
|
|
# },
|
|
# {
|
|
# "feeTierId": "8",
|
|
# "staticSpreadFee": "0.00060000",
|
|
# "isDislocationEnabled": True
|
|
# },
|
|
# {
|
|
# "feeTierId": "9",
|
|
# "staticSpreadFee": "0.00100000",
|
|
# "isDislocationEnabled": False
|
|
# }
|
|
# ],
|
|
# "marketType": "DATED_FUTURE",
|
|
# "contractMultiplier": "1",
|
|
# "settlementAssetSymbol": "USDC",
|
|
# "underlyingQuoteSymbol": "USDC",
|
|
# "underlyingBaseSymbol": "BTC",
|
|
# "openInterestLimitUSD": "100000000.0000",
|
|
# "concentrationRiskPercentage": "100.00",
|
|
# "concentrationRiskThresholdUSD": "30000000.0000",
|
|
# "expiryDatetime": "2025-05-16T08:00:00.000Z",
|
|
# "priceBuffer": "0.1",
|
|
# "feeGroupId": "4"
|
|
# }
|
|
#
|
|
# option
|
|
# {
|
|
# "marketId": "20997",
|
|
# "symbol": "BTC-USDC-20260130-160000-P",
|
|
# "quoteAssetId": "5",
|
|
# "baseAssetId": "1",
|
|
# "quoteSymbol": "USDC",
|
|
# "baseSymbol": "BTC",
|
|
# "quotePrecision": "4",
|
|
# "basePrecision": "8",
|
|
# "pricePrecision": "4",
|
|
# "quantityPrecision": "8",
|
|
# "costPrecision": "4",
|
|
# "minQuantityLimit": "0.00050000",
|
|
# "maxQuantityLimit": "200.00000000",
|
|
# "maxPriceLimit": null,
|
|
# "minPriceLimit": null,
|
|
# "maxCostLimit": null,
|
|
# "minCostLimit": null,
|
|
# "timeZone": "Etc/UTC",
|
|
# "tickSize": "10.0000",
|
|
# "makerFee": "0",
|
|
# "takerFee": "2",
|
|
# "roundingCorrectionFactor": "0.00000100",
|
|
# "makerMinLiquidityAddition": "-1",
|
|
# "orderTypes": ["LMT", "MKT", "STOP_LIMIT", "POST_ONLY"],
|
|
# "spotTradingEnabled": True,
|
|
# "marginTradingEnabled": True,
|
|
# "marketEnabled": True,
|
|
# "createOrderEnabled": True,
|
|
# "cancelOrderEnabled": True,
|
|
# "amendOrderEnabled": True,
|
|
# "marketType": "OPTION",
|
|
# "contractMultiplier": "1",
|
|
# "settlementAssetSymbol": "USDC",
|
|
# "underlyingQuoteSymbol": "USDC",
|
|
# "underlyingBaseSymbol": "BTC",
|
|
# "openInterestLimitUSD": "100000000.0000",
|
|
# "concentrationRiskPercentage": "100.00",
|
|
# "concentrationRiskThresholdUSD": "30000000.0000",
|
|
# "expiryDatetime": "2026-01-30T08:00:00.000Z",
|
|
# "priceBuffer": "0",
|
|
# "feeGroupId": "10",
|
|
# "optionStrikePrice": "160000.0000",
|
|
# "optionType": "PUT",
|
|
# "premiumCapRatio": "0.1000"
|
|
# }
|
|
#
|
|
id = self.safe_string(market, 'symbol')
|
|
baseId = self.safe_string(market, 'baseSymbol')
|
|
quoteId = self.safe_string(market, 'quoteSymbol')
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
symbol = base + '/' + quote
|
|
basePrecision = self.safe_string(market, 'basePrecision')
|
|
quotePrecision = self.safe_string(market, 'quotePrecision')
|
|
amountPrecision = self.safe_string(market, 'quantityPrecision')
|
|
pricePrecision = self.safe_string(market, 'pricePrecision')
|
|
costPrecision = self.safe_string(market, 'costPrecision')
|
|
minQuantityLimit = self.safe_string(market, 'minQuantityLimit')
|
|
maxQuantityLimit = self.safe_string(market, 'maxQuantityLimit')
|
|
minPriceLimit = self.safe_string(market, 'minPriceLimit')
|
|
maxPriceLimit = self.safe_string(market, 'maxPriceLimit')
|
|
minCostLimit = self.safe_string(market, 'minCostLimit')
|
|
maxCostLimit = self.safe_string(market, 'maxCostLimit')
|
|
settleId = self.safe_string(market, 'settlementAssetSymbol')
|
|
settle = self.safe_currency_code(settleId)
|
|
type = self.parse_market_type(self.safe_string(market, 'marketType'), 'spot')
|
|
spot: Bool = False
|
|
swap: Bool = False
|
|
future: Bool = False
|
|
option: Bool = False
|
|
contract: Bool = True
|
|
linear: Bool = None
|
|
inverse: Bool = None
|
|
expiryDatetime: Str = None
|
|
contractSize: Num = None
|
|
optionType: Str = None
|
|
strike: Num = None
|
|
margin: Bool = False
|
|
if type == 'spot':
|
|
spot = True
|
|
contract = False
|
|
margin = self.safe_bool(market, 'marginTradingEnabled')
|
|
else:
|
|
contractSize = self.safe_number(market, 'contractMultiplier')
|
|
symbol += ':' + settle
|
|
linear = settle == quote
|
|
inverse = not linear
|
|
if type == 'swap':
|
|
swap = True
|
|
else:
|
|
expiryDatetime = self.safe_string(market, 'expiryDatetime')
|
|
idParts = id.split('-')
|
|
datePart = self.safe_string(idParts, 2)
|
|
dateYmd = datePart[2:]
|
|
symbol += '-' + dateYmd
|
|
if type == 'future':
|
|
future = True
|
|
elif type == 'option':
|
|
option = True
|
|
optionType = self.safe_string_lower(market, 'optionType')
|
|
strike = self.parse_to_numeric(self.safe_string(market, 'optionStrikePrice'))
|
|
symbol += '-' + self.number_to_string(strike) + '-' + self.safe_string(idParts, 4)
|
|
return self.safe_market_structure({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'baseId': baseId,
|
|
'quote': quote,
|
|
'quoteId': quoteId,
|
|
'settle': settle,
|
|
'settleId': settleId,
|
|
'type': type,
|
|
'spot': spot,
|
|
'margin': margin,
|
|
'swap': swap,
|
|
'future': future,
|
|
'option': option,
|
|
'contract': contract,
|
|
'linear': linear,
|
|
'inverse': inverse,
|
|
'taker': self.fees['trading']['taker'],
|
|
'maker': self.fees['trading']['maker'],
|
|
'contractSize': contractSize,
|
|
'expiry': self.parse8601(expiryDatetime),
|
|
'expiryDatetime': expiryDatetime,
|
|
'strike': strike,
|
|
'optionType': optionType,
|
|
'limits': {
|
|
'amount': {
|
|
'min': self.parse_number(minQuantityLimit),
|
|
'max': self.parse_number(maxQuantityLimit),
|
|
},
|
|
'price': {
|
|
'min': self.parse_number(minPriceLimit),
|
|
'max': self.parse_number(maxPriceLimit),
|
|
},
|
|
'cost': {
|
|
'min': self.parse_number(minCostLimit),
|
|
'max': self.parse_number(maxCostLimit),
|
|
},
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'precision': {
|
|
'amount': self.parse_number(self.parse_precision(amountPrecision)),
|
|
'price': self.parse_number(self.parse_precision(pricePrecision)),
|
|
'cost': self.parse_number(self.parse_precision(costPrecision)),
|
|
'base': self.parse_number(self.parse_precision(basePrecision)),
|
|
'quote': self.parse_number(self.parse_precision(quotePrecision)),
|
|
},
|
|
'active': self.safe_bool(market, 'marketEnabled'),
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
|
|
def parse_market_type(self, type: str, defaultType: Str = None) -> str:
|
|
types = {
|
|
'SPOT': 'spot',
|
|
'PERPETUAL': 'swap',
|
|
'DATED_FUTURE': 'future',
|
|
'OPTION': 'option',
|
|
}
|
|
return self.safe_string(types, type, defaultType)
|
|
|
|
def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches 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/#get-/v1/markets/-symbol-/orderbook/hybrid
|
|
|
|
: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(not used by bullish)
|
|
: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
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
response = self.publicGetV1MarketsSymbolOrderbookHybrid(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "bids": [
|
|
# {
|
|
# "price": "1.00000000",
|
|
# "priceLevelQuantity": "1.00000000"
|
|
# }
|
|
# ],
|
|
# "asks": [
|
|
# {
|
|
# "price": "1.00000000",
|
|
# "priceLevelQuantity": "1.00000000"
|
|
# }
|
|
# ],
|
|
# "datetime": "2021-05-20T01:01:01.000Z",
|
|
# "timestamp": "1621490985000",
|
|
# "sequenceNumber": 999
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer(response, 'timestamp')
|
|
return self.parse_order_book(response, symbol, timestamp, 'bids', 'asks', 'price', 'priceLevelQuantity')
|
|
|
|
def fetch_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/#get-/v1/markets/-symbol-/trades
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/markets/-symbol-/trades
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch(max 100)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest trade to fetch
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
|
|
"""
|
|
self.load_markets()
|
|
maxLimit = 100
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate')
|
|
if paginate:
|
|
params = self.handle_pagination_params('fetchTrades', since, params)
|
|
return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params, maxLimit)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
params = self.handle_since_and_until(since, params)
|
|
if limit is not None:
|
|
request['_pageSize'] = self.get_closest_limit(limit)
|
|
response = self.publicGetV1HistoryMarketsSymbolTrades(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "tradeId": "100178000000367159",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "103891.8977",
|
|
# "quantity": "0.00029411",
|
|
# "quoteAmount": "30.5556",
|
|
# "side": "BUY",
|
|
# "isTaker": True,
|
|
# "createdAtTimestamp": "1747768055826",
|
|
# "createdAtDatetime": "2025-05-20T19:07:35.826Z"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/trades
|
|
|
|
: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 int [params.until]: the latest time in ms to fetch trades for
|
|
:param str [params.orderId]: the order id to fetch trades for
|
|
:param str [params.clientOrderId]: the client order id to fetch trades for
|
|
:param str [params.tradingAccountId]: the trading account id to fetch trades for
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
market: Market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
response = None
|
|
if clientOrderId is not None:
|
|
response = self.privateGetV1TradesClientOrderIdClientOrderId(self.extend(request, params))
|
|
else:
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
|
if paginate:
|
|
params = self.handle_pagination_params('fetchMyTrades', since, params)
|
|
return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100)
|
|
params = self.handle_since_and_until(since, params)
|
|
if limit is not None:
|
|
request['_pageSize'] = self.get_closest_limit(limit)
|
|
#
|
|
# [
|
|
# {
|
|
# "baseFee": "0.00000000",
|
|
# "createdAtDatetime": "2025-05-18T15:57:28.132Z",
|
|
# "createdAtTimestamp": "1747583848132",
|
|
# "handle": null,
|
|
# "isTaker": True,
|
|
# "orderId": "844242293909618689",
|
|
# "price": "103942.7048",
|
|
# "publishedAtTimestamp": "1747769786131",
|
|
# "quantity": "1.00000000",
|
|
# "quoteAmount": "103942.7048",
|
|
# "quoteFee": "0.0000",
|
|
# "side": "BUY",
|
|
# "symbol": "BTCUSDC",
|
|
# "tradeId": "100178000000288892"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
response = self.privateGetV1HistoryTrades(self.extend(request, params))
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
fetch all the trades made from a single order
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/trades
|
|
|
|
:param str id: order id
|
|
: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 to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: the client order id to fetch trades for
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
|
|
"""
|
|
self.load_markets()
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is None:
|
|
params = self.extend({'orderId': id}, params)
|
|
return self.fetch_my_trades(symbol, since, limit, params)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# fetchTrades
|
|
# [
|
|
# {
|
|
# "tradeId": "100178000000367159",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "103891.8977",
|
|
# "quantity": "0.00029411",
|
|
# "quoteAmount": "30.5556",
|
|
# "side": "BUY",
|
|
# "isTaker": True,
|
|
# "createdAtTimestamp": "1747768055826",
|
|
# "createdAtDatetime": "2025-05-20T19:07:35.826Z"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
# [
|
|
# {
|
|
# "tradeId": "100020000000000060",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "1.00000000",
|
|
# "quantity": "1.00000000",
|
|
# "side": "BUY",
|
|
# "isTaker": True,
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000"
|
|
# }
|
|
# ]
|
|
#
|
|
# fetchMyTrades
|
|
# [
|
|
# {
|
|
# "baseFee": "0.00000000",
|
|
# "createdAtDatetime": "2025-05-18T15:57:28.132Z",
|
|
# "createdAtTimestamp": "1747583848132",
|
|
# "handle": null,
|
|
# "isTaker": True,
|
|
# "orderId": "844242293909618689",
|
|
# "price": "103942.7048",
|
|
# "publishedAtTimestamp": "1747769786131",
|
|
# "quantity": "1.00000000",
|
|
# "quoteAmount": "103942.7048",
|
|
# "quoteFee": "0.0000",
|
|
# "side": "BUY",
|
|
# "symbol": "BTCUSDC",
|
|
# "tradeId": "100178000000288892"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
marketId = self.safe_string(trade, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
timestamp = self.safe_integer(trade, 'createdAtTimestamp')
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'quantity')
|
|
side = self.safe_string_lower(trade, 'side')
|
|
isTaker = self.safe_bool(trade, 'isTaker')
|
|
currency = market['quote']
|
|
code = self.safe_currency_code(currency)
|
|
feeCost = self.safe_number(trade, 'quoteFee')
|
|
fee = None
|
|
if feeCost is not None:
|
|
fee = {'currency': code, 'cost': feeCost}
|
|
takerOrMaker = None
|
|
if isTaker:
|
|
takerOrMaker = 'taker'
|
|
else:
|
|
takerOrMaker = 'maker'
|
|
orderId = self.safe_string(trade, 'orderId')
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'id': self.safe_string(trade, 'tradeId'),
|
|
'order': orderId,
|
|
'type': None,
|
|
'takerOrMaker': takerOrMaker,
|
|
'side': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches 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/#get-/v1/markets/-symbol-/tick
|
|
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
response = self.publicGetV1MarketsSymbolTick(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "high": "1.00000000",
|
|
# "low": "1.00000000",
|
|
# "bestBid": "1.00000000",
|
|
# "bidVolume": "1.00000000",
|
|
# "bestAsk": "1.00000000",
|
|
# "askVolume": "1.00000000",
|
|
# "vwap": "1.00000000",
|
|
# "open": "1.00000000",
|
|
# "close": "1.00000000",
|
|
# "last": "1.00000000",
|
|
# "change": "1.00000000",
|
|
# "percentage": "1.00000000",
|
|
# "average": "1.00000000",
|
|
# "baseVolume": "1.00000000",
|
|
# "quoteVolume": "1.00000000",
|
|
# "bancorPrice": "1.00000000",
|
|
# "markPrice": "19999.00",
|
|
# "fundingRate": "0.01",
|
|
# "openInterest": "100000.32452",
|
|
# "lastTradeDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "lastTradeTimestamp": "1621490985000",
|
|
# "lastTradeQuantity": "1.00000000",
|
|
# "ammData": [
|
|
# {
|
|
# "feeTierId": "1",
|
|
# "bidSpreadFee": "0.00040000",
|
|
# "askSpreadFee": "0.00040000",
|
|
# "baseReservesQuantity": "245.56257825",
|
|
# "quoteReservesQuantity": "3424383.3629",
|
|
# "currentPrice": "16856.0000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
return self.parse_ticker(response, market)
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# {
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "high": "1.00000000",
|
|
# "low": "1.00000000",
|
|
# "bestBid": "1.00000000",
|
|
# "bidVolume": "1.00000000",
|
|
# "bestAsk": "1.00000000",
|
|
# "askVolume": "1.00000000",
|
|
# "vwap": "1.00000000",
|
|
# "open": "1.00000000",
|
|
# "close": "1.00000000",
|
|
# "last": "1.00000000",
|
|
# "change": "1.00000000",
|
|
# "percentage": "1.00000000",
|
|
# "average": "1.00000000",
|
|
# "baseVolume": "1.00000000",
|
|
# "quoteVolume": "1.00000000",
|
|
# "bancorPrice": "1.00000000",
|
|
# "markPrice": "19999.00",
|
|
# "fundingRate": "0.01",
|
|
# "openInterest": "100000.32452",
|
|
# "lastTradeDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "lastTradeTimestamp": "1621490985000",
|
|
# "lastTradeQuantity": "1.00000000",
|
|
# "ammData": [
|
|
# {
|
|
# "feeTierId": "1",
|
|
# "bidSpreadFee": "0.00040000",
|
|
# "askSpreadFee": "0.00040000",
|
|
# "baseReservesQuantity": "245.56257825",
|
|
# "quoteReservesQuantity": "3424383.3629",
|
|
# "currentPrice": "16856.0000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
marketId = self.safe_string(ticker, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer(ticker, 'createdAtTimestamp')
|
|
return self.safe_ticker({
|
|
'symbol': market['symbol'],
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string(ticker, 'high'),
|
|
'low': self.safe_string(ticker, 'low'),
|
|
'bid': self.safe_string_2(ticker, 'bid', 'bestBid'),
|
|
'bidVolume': self.safe_string(ticker, 'bidVolume'),
|
|
'ask': self.safe_string_2(ticker, 'ask', 'bestAsk'),
|
|
'askVolume': self.safe_string(ticker, 'askVolume'),
|
|
'vwap': self.safe_string(ticker, 'vwap'),
|
|
'open': self.safe_string(ticker, 'open'),
|
|
'close': self.safe_string(ticker, 'close'),
|
|
'last': self.safe_string(ticker, 'last'),
|
|
'previousClose': None,
|
|
'change': self.safe_string(ticker, 'change'),
|
|
'percentage': self.safe_string(ticker, 'percentage'),
|
|
'average': self.safe_string(ticker, 'average'),
|
|
'baseVolume': self.safe_string(ticker, 'baseVolume'),
|
|
'quoteVolume': self.safe_string(ticker, 'quoteVolume'),
|
|
'markPrice': self.safe_string(ticker, 'markPrice'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
def safe_deterministic_call(self, method: str, symbol: Str = None, since: Int = None, limit: Int = None, timeframe: Str = None, params={}):
|
|
maxRetries = None
|
|
maxRetries, params = self.handle_option_and_params(params, method, 'maxRetries', 3)
|
|
errors = 0
|
|
params = self.omit(params, 'until')
|
|
# the exchange returns the most recent data, so we do not need to pass until into paginated calls
|
|
# the correct util value will be calculated inside of the method
|
|
while(errors <= maxRetries):
|
|
try:
|
|
if timeframe and method != 'fetchFundingRateHistory':
|
|
return getattr(self, method)(symbol, timeframe, since, limit, params)
|
|
else:
|
|
return getattr(self, method)(symbol, since, limit, params)
|
|
except Exception as e:
|
|
if isinstance(e, RateLimitExceeded):
|
|
raise e # if we are rate limited, we should not retry and fail fast
|
|
errors += 1
|
|
if errors > maxRetries:
|
|
raise e
|
|
return []
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/markets/-symbol-/candle
|
|
|
|
: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(max 100)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest entry
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
maxLimit = 100
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'timeBucket': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
'_pageSize': maxLimit,
|
|
}
|
|
request, params = self.handle_until_option('createdAtDatetime[lte]', request, params)
|
|
until = self.safe_integer(request, 'createdAtDatetime[lte]')
|
|
duration = self.parse_timeframe(timeframe)
|
|
maxDelta = 1000 * duration * maxLimit
|
|
startTime = since
|
|
# both of since and until are required
|
|
if startTime is None and until is None:
|
|
until = self.milliseconds()
|
|
startTime = until - maxDelta
|
|
elif startTime is None:
|
|
startTime = until - maxDelta
|
|
elif until is None:
|
|
until = self.sum(startTime, maxDelta)
|
|
request['createdAtDatetime[gte]'] = self.iso8601(startTime)
|
|
request['createdAtDatetime[lte]'] = self.iso8601(until)
|
|
response = self.publicGetV1MarketsSymbolCandle(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "open": "100846.7490",
|
|
# "high": "100972.4001",
|
|
# "low": "100840.8129",
|
|
# "close": "100972.2602",
|
|
# "volume": "30.56064890",
|
|
# "createdAtTimestamp": "1746720540000",
|
|
# "createdAtDatetime": "2025-05-08T16:09:00.000Z",
|
|
# "publishedAtTimestamp": "1746720636007"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
return self.parse_ohlcvs(response, market, timeframe, since, limit)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
return [
|
|
self.safe_integer(ohlcv, 'createdAtTimestamp'),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
self.safe_number(ohlcv, 'volume'),
|
|
]
|
|
|
|
def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[FundingRateHistory]:
|
|
"""
|
|
fetches historical funding rate prices
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/markets/-symbol-/funding-rate
|
|
|
|
:param str symbol: unified symbol of the market to fetch the funding rate history for
|
|
:param int [since]: not sent to exchange api, exchange api always returns the most recent data, only used to filter exchange response
|
|
:param int [limit]: the maximum amount of funding rate structures to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-history-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument')
|
|
self.load_markets()
|
|
maxLimit = 100
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate')
|
|
if paginate:
|
|
params = self.handle_pagination_params('fetchFundingRateHistory', since, params)
|
|
return self.fetch_paginated_call_dynamic('fetchFundingRateHistory', symbol, since, limit, params, maxLimit)
|
|
market = self.market(symbol)
|
|
if not market['swap']:
|
|
raise BadRequest(self.id + ' fetchFundingRateHistory() supports swap markets only')
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['_pageSize'] = self.get_closest_limit(limit)
|
|
params = self.handle_since_and_until(since, params, 'updatedAtDatetime[gte]', 'updatedAtDatetime[lte]')
|
|
response = self.publicGetV1HistoryMarketsSymbolFundingRate(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "fundingRate": "0.00125",
|
|
# "updatedAtDatetime": "2025-05-18T09:06:04.074Z"
|
|
# },
|
|
# {
|
|
# "fundingRate": "0.00125",
|
|
# "updatedAtDatetime": "2025-05-18T08:59:59.033Z"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
rates = []
|
|
result = self.to_array(response)
|
|
for i in range(0, len(result)):
|
|
entry = result[i]
|
|
datetime = self.safe_string(entry, 'updatedAtDatetime')
|
|
rates.append({
|
|
'info': entry,
|
|
'symbol': symbol,
|
|
'fundingRate': self.safe_number(entry, 'fundingRate'),
|
|
'timestamp': self.parse8601(datetime),
|
|
'datetime': datetime,
|
|
})
|
|
sorted = self.sort_by(rates, 'timestamp')
|
|
return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit)
|
|
|
|
def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple orders made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--orders
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--history
|
|
|
|
: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(5, 25, 50, 100, default is 25)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest order to fetch
|
|
:param str [params.tradingAccountId]: the trading account id(mandatory parameter)
|
|
:param str [params.orderId]: the id of the order to fetch for
|
|
:param str [params.clientOrderId]: the client id of the order to fetch for
|
|
:param str [params.status]: filter by order status, 'OPEN', 'CANCELLED', 'CLOSED', 'REJECTED'
|
|
:param bool [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
paginate = self.safe_bool(params, 'paginate', False)
|
|
if paginate:
|
|
params = self.handle_pagination_params('fetchOrders', since, params)
|
|
return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params, 100)
|
|
market = None
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
params = self.handle_since_and_until(since, params)
|
|
if limit is not None:
|
|
request['_pageSize'] = self.get_closest_limit(limit)
|
|
method = 'privateGetV2HistoryOrders'
|
|
method, params = self.handle_option_and_params(params, 'fetchOrders', 'method', method)
|
|
response = None
|
|
if method == 'privateGetV2Orders':
|
|
#
|
|
# [
|
|
# {
|
|
# "clientOrderId": "187",
|
|
# "orderId": "297735387747975681",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "1.00000000",
|
|
# "averageFillPrice": "1.00000000",
|
|
# "stopPrice": "1.00000000",
|
|
# "allowBorrow": False,
|
|
# "quantity": "1.00000000",
|
|
# "quantityFilled": "1.00000000",
|
|
# "quoteAmount": "1.00000000",
|
|
# "baseFee": "0.00100000",
|
|
# "quoteFee": "0.0010",
|
|
# "borrowedBaseQuantity": "1.00000000",
|
|
# "borrowedQuoteQuantity": "1.00000000",
|
|
# "isLiquidation": False,
|
|
# "side": "BUY",
|
|
# "type": "LMT",
|
|
# "timeInForce": "GTC",
|
|
# "status": "OPEN",
|
|
# "statusReason": "User cancelled",
|
|
# "statusReasonCode": "1002",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# }
|
|
# ]
|
|
#
|
|
response = self.privateGetV2Orders(self.extend(request, params))
|
|
elif method == 'privateGetV2HistoryOrders':
|
|
response = self.privateGetV2HistoryOrders(self.extend(request, params))
|
|
else:
|
|
raise BadRequest(self.id + ' fetchOrders() method parameter must be either "privateGetV2Orders" or "privateGetV2HistoryOrders"')
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
def handle_pagination_params(self, method: str, since: Int = None, params: dict = {}) -> dict:
|
|
ninetyDays = 90 * 24 * 60 * 60 * 1000
|
|
now = self.milliseconds()
|
|
allowedSince = now - ninetyDays
|
|
if (since is not None) and (since < allowedSince):
|
|
raise BadRequest(self.id + ' ' + method + '() only allows fetching entries up to 90 days in the past')
|
|
params = self.omit(params, 'paginate')
|
|
params = self.extend(params, {'paginationDirection': 'backward'})
|
|
until = self.safe_integer(params, 'until')
|
|
if until is None:
|
|
params = self.extend(params, {'until': now})
|
|
return params
|
|
|
|
def handle_since_and_until(self, since: Int = None, params: dict = {}, sinceKey: Str = 'createdAtDatetime[gte]', untilKey: Str = 'createdAtDatetime[lte]') -> dict:
|
|
until = self.safe_integer(params, 'until')
|
|
if (since is not None) or (until is not None):
|
|
timeDelta = 7 * 24 * 60 * 60 * 1000 # 7 days
|
|
if since is None:
|
|
since = until - timeDelta
|
|
params = self.omit(params, 'until')
|
|
elif until is None:
|
|
until = self.sum(since, timeDelta)
|
|
now = self.milliseconds()
|
|
if until > now:
|
|
until = now
|
|
sinceDate = self.iso8601(since)
|
|
untilDate = self.iso8601(until)
|
|
params[sinceKey] = sinceDate
|
|
params[untilKey] = untilDate
|
|
return params
|
|
|
|
def get_closest_limit(self, limit: Int) -> Int:
|
|
pageSize = 5
|
|
if (limit > 5) and (limit < 26):
|
|
pageSize = 25
|
|
elif (limit > 25) and (limit < 51):
|
|
pageSize = 50
|
|
elif limit > 50:
|
|
pageSize = 100
|
|
return pageSize
|
|
|
|
def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--history
|
|
|
|
: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(mandatory parameter)
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'OPEN',
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple canceled orders made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--orders
|
|
|
|
:param str symbol: unified market symbol of the canceled orders
|
|
:param int [since]: timestamp in ms of the earliest order
|
|
:param int [limit]: the max number of canceled orders to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.tradingAccountId]: the trading account id(mandatory parameter)
|
|
:returns dict: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'CANCELLED',
|
|
'method': 'privateGetV2Orders', # current endpoint distinquishes between CLOSED and CANCELLED orders
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple closed orders made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--orders
|
|
|
|
:param str symbol: unified market symbol of the closed orders
|
|
:param int [since]: timestamp in ms of the earliest order
|
|
:param int [limit]: the max number of closed orders to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['tradingAccountId']: the trading account id(mandatory parameter)
|
|
:returns dict: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'CLOSED',
|
|
'method': 'privateGetV2Orders', # current endpoint distinquishes between CLOSED and CANCELLED orders
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple canceled orders made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--history
|
|
|
|
:param str symbol: unified market symbol of the closed orders
|
|
:param int [since]: timestamp in ms of the earliest order
|
|
:param int [limit]: the max number of closed orders to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.tradingAccountId]: the trading account id(mandatory parameter)
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'CLOSED',
|
|
'method': 'privateGetV2HistoryOrders', # current endpoint returns both CLOSED and CANCELLED orders
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v2/orders/-orderId-
|
|
|
|
:param str id: the order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.traidingAccountId]: the trading account id(mandatory parameter)
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'orderId': id,
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
response = self.privateGetV2OrdersOrderId(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "clientOrderId": "187",
|
|
# "orderId": "297735387747975680",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "1.00000000",
|
|
# "averageFillPrice": "1.00000000",
|
|
# "stopPrice": "1.00000000",
|
|
# "allowBorrow": False,
|
|
# "quantity": "1.00000000",
|
|
# "quantityFilled": "1.00000000",
|
|
# "quoteAmount": "1.00000000",
|
|
# "baseFee": "0.00100000",
|
|
# "quoteFee": "0.0010",
|
|
# "borrowedBaseQuantity": "1.00000000",
|
|
# "borrowedQuoteQuantity": "1.00000000",
|
|
# "isLiquidation": False,
|
|
# "side": "BUY",
|
|
# "type": "LMT",
|
|
# "timeInForce": "GTC",
|
|
# "status": "OPEN",
|
|
# "statusReason": "User cancelled",
|
|
# "statusReasonCode": "1002",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# }
|
|
#
|
|
return self.parse_order(response, market)
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
|
|
"""
|
|
create a trade order
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v2/orders
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit' or 'STOP_LIMIT' or 'POST_ONLY'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: a custom client order id
|
|
:param float [params.triggerPrice]: the price at which a stop order is triggered at
|
|
:param str [params.timeInForce]: the time in force for the order, either 'GTC'(Good Till Cancelled) or 'IOC'(Immediate or Cancel), default is 'GTC'
|
|
:param bool [params.allowBorrow]: if True, the order will be allowed to borrow assets to fulfill the order(default is False)
|
|
:param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately(default is False)
|
|
:param str params['traidingAccountId']: the trading account id(mandatory parameter)
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'commandType': 'V3CreateOrder',
|
|
'symbol': market['id'],
|
|
'side': side.upper(),
|
|
'quantity': self.amount_to_precision(symbol, amount),
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
isMarketOrder = ((type == 'market') or type == 'MARKET')
|
|
postOnly = False
|
|
postOnly, params = self.handle_post_only(isMarketOrder, type == 'POST_ONLY', params)
|
|
if postOnly:
|
|
type = 'POST_ONLY'
|
|
timeInForce = 'GTC' # is mandatory
|
|
timeInForce, params = self.handle_option_and_params(params, 'createOrder', 'timeInForce', timeInForce)
|
|
params['timeInForce'] = timeInForce.upper()
|
|
if not isMarketOrder:
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
triggerPrice = self.safe_string(params, 'triggerPrice')
|
|
if triggerPrice is not None:
|
|
if isMarketOrder:
|
|
raise NotSupported(self.id + ' createOrder() does not support market trigger orders')
|
|
request['stopPrice'] = self.price_to_precision(symbol, triggerPrice)
|
|
type = 'STOP_LIMIT'
|
|
params = self.omit(params, 'triggerPrice')
|
|
request['type'] = type.upper()
|
|
response = self.privatePostV2Orders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "message": "Command acknowledged - CreateOrder",
|
|
# "requestId": "633910976353665024",
|
|
# "orderId": "633910775316480001",
|
|
# "clientOrderId": "1234567"
|
|
# }
|
|
#
|
|
return self.parse_order(response, market)
|
|
|
|
def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
|
|
"""
|
|
edit a trade limit order
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v2/command-amend
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market to create an order in
|
|
:param str [type]: 'limit' or 'POST_ONLY'
|
|
:param str [side]: not used by bullish editOrder
|
|
:param float [amount]: how much of the currency you want to trade in units of the base currency
|
|
:param float [price]: the price for the order, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.traidingAccountId]: the trading account id(mandatory parameter)
|
|
:param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately(default is False)
|
|
:param str [params.clientOrderId]: a unique identifier for the order, automatically generated if not sent
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'commandType': 'V1AmendOrder',
|
|
'symbol': market['id'],
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is None:
|
|
request['orderId'] = id
|
|
if type is not None:
|
|
request['type'] = type.upper()
|
|
postOnly = self.safe_bool(params, 'postOnly', False)
|
|
if postOnly:
|
|
params = self.omit(params, 'postOnly')
|
|
request['type'] = 'POST_ONLY'
|
|
if amount is not None:
|
|
request['quantity'] = self.amount_to_precision(symbol, amount)
|
|
if price is not None:
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
response = self.privatePostV2Command(self.extend(request, params))
|
|
return self.parse_order(response, market)
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
cancels an open order
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v2/command-cancellations
|
|
|
|
:param str [id]: order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['commandType']: the command type, default is 'V3CancelOrder'(mandatory parameter)
|
|
:param str [params.traidingAccountId]: the trading account id(mandatory parameter)
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'tradingAccountId': tradingAccountId,
|
|
'commandType': self.safe_string(params, 'commandType', 'V3CancelOrder'),
|
|
'orderId': id,
|
|
}
|
|
response = self.privatePostV2Command(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "message": "Command acknowledged - CancelOrder",
|
|
# "requestId": "844658480774644736",
|
|
# "orderId": "297735387747975680",
|
|
# "clientOrderId": null
|
|
# }
|
|
#
|
|
return self.parse_order(response, market)
|
|
|
|
def cancel_all_orders(self, symbol: Str = None, params={}) -> List[Order]:
|
|
"""
|
|
cancel all open orders in a market
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v2/command-cancellations
|
|
|
|
:param str [symbol]: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['traidingAccountId']: the trading account id(mandatory parameter)
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
request['commandType'] = 'V1CancelAllOrdersByMarket'
|
|
else:
|
|
request['commandType'] = 'V1CancelAllOrders'
|
|
response = self.privatePostV2Command(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "message": "Command acknowledged - CancelAllOrders",
|
|
# "requestId": "633900538459062272"
|
|
# }
|
|
#
|
|
orders = [response]
|
|
return self.parse_orders(orders, market)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# fetchOrders, fetchOrder
|
|
# {
|
|
# "clientOrderId": "187",
|
|
# "orderId": "297735387747975680",
|
|
# "symbol": "BTCUSDC",
|
|
# "price": "1.00000000",
|
|
# "averageFillPrice": "1.00000000",
|
|
# "stopPrice": "1.00000000",
|
|
# "allowBorrow": False,
|
|
# "quantity": "1.00000000",
|
|
# "quantityFilled": "1.00000000",
|
|
# "quoteAmount": "1.00000000",
|
|
# "baseFee": "0.00100000",
|
|
# "quoteFee": "0.0010",
|
|
# "borrowedBaseQuantity": "1.00000000",
|
|
# "borrowedQuoteQuantity": "1.00000000",
|
|
# "isLiquidation": False,
|
|
# "side": "BUY",
|
|
# "type": "LMT",
|
|
# "timeInForce": "GTC",
|
|
# "status": "OPEN",
|
|
# "statusReason": "User cancelled",
|
|
# "statusReasonCode": "1002",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# }
|
|
#
|
|
# createOrder
|
|
# {
|
|
# "message": "Command acknowledged - CreateOrder",
|
|
# "requestId": "633910976353665024",
|
|
# "orderId": "633910775316480001",
|
|
# "clientOrderId": "1234567"
|
|
# }
|
|
#
|
|
# cancelOrder
|
|
# {
|
|
# "message": "Command acknowledged - CancelOrder",
|
|
# "requestId": "633910976353665024",
|
|
# "orderId": "633910775316480001"
|
|
# }
|
|
#
|
|
# cancelAllOrders
|
|
# {
|
|
# "message": "Command acknowledged - CancelAllOrders",
|
|
# "requestId": "633900538459062272"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(order, 'symbol')
|
|
if market is None:
|
|
market = self.safe_market(marketId)
|
|
symbol = self.safe_symbol(marketId, market)
|
|
id = self.safe_string(order, 'orderId')
|
|
timestamp = self.safe_integer(order, 'createdAtTimestamp')
|
|
type = self.safe_string(order, 'type')
|
|
side = self.safe_string_lower(order, 'side')
|
|
price = self.safe_string(order, 'price')
|
|
amount = self.safe_string(order, 'quantity')
|
|
filled = self.safe_string(order, 'quantityFilled')
|
|
status = self.parse_order_status(self.safe_string(order, 'status'))
|
|
if status == 'closed':
|
|
statusReason = self.safe_string(order, 'statusReason')
|
|
if statusReason == 'User cancelled':
|
|
status = 'canceled'
|
|
timeInForce = self.safe_string(order, 'timeInForce')
|
|
stopPrice = self.safe_string(order, 'stopPrice')
|
|
cost = self.safe_string(order, 'quoteAmount')
|
|
fee = {}
|
|
quoteFee = self.safe_number(order, 'quoteFee')
|
|
if quoteFee is not None:
|
|
fee['cost'] = quoteFee
|
|
fee['currency'] = market['quote']
|
|
average = self.safe_string(order, 'averageFillPrice')
|
|
return self.safe_order({
|
|
'id': id,
|
|
'clientOrderId': self.safe_string(order, 'clientOrderId'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'status': status,
|
|
'symbol': symbol,
|
|
'type': self.parse_order_type(type),
|
|
'timeInForce': timeInForce,
|
|
'postOnly': type == 'POST_ONLY',
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': stopPrice,
|
|
'amount': amount,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'cost': cost,
|
|
'trades': None,
|
|
'fee': fee,
|
|
'info': order,
|
|
'average': average,
|
|
}, market)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'OPEN': 'open',
|
|
'CLOSED': 'closed',
|
|
'CANCELLED': 'canceled',
|
|
'REJECTED': 'rejected',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order_type(self, type: Str):
|
|
types: dict = {
|
|
'LMT': 'limit',
|
|
'MKT': 'market',
|
|
'POST_ONLY': 'limit',
|
|
'STOP_LIMIT': 'limit',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch history of deposits and withdrawals
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/wallets/transactions
|
|
|
|
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
|
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
|
:param int [limit]: max number of deposit/withdrawals to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
request: dict = {}
|
|
request, params = self.handle_until_option('createdAtDatetime[lte]', request, params)
|
|
until = self.safe_integer(request, 'createdAtDatetime[lte]')
|
|
if until is not None:
|
|
request['createdAtDatetime[lte]'] = self.iso8601(until)
|
|
if since is not None:
|
|
request['createdAtDatetime[gte]'] = self.iso8601(since)
|
|
response = self.privateGetV1WalletsTransactions(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "custodyTransactionId": "0x791fc85f16a84cbd5250d5517ecad497f564d2e5cc54d31466fe70b952fd58da",
|
|
# "direction": "DEPOSIT",
|
|
# "quantity": "150",
|
|
# "symbol": "USDC",
|
|
# "fee": "0",
|
|
# "memo": "0x34625d5f0b6575503a0669994dea24271bfbd443",
|
|
# "createdAtDateTime": "2025-11-04T14:31:17.000Z",
|
|
# "updatedAtDateTime": "2025-11-04T14:44:17.500Z",
|
|
# "status": "COMPLETE",
|
|
# "statusReason": "OK",
|
|
# "network": "ETH",
|
|
# "transactionDetails": {
|
|
# "address": "0x34625d5f0b6575503a0669994dea24271bfbd443",
|
|
# "blockchainTxId": "0x791fc85f16a84cbd5250d5517ecad497f564d2e5cc54d31466fe70b952fd58da",
|
|
# "swiftUetr": null,
|
|
# "sources": [
|
|
# {
|
|
# "address": "0x2653435d52a5f49551ebb757f25b2c8bb954859b"
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
# ],
|
|
# "links": {
|
|
# "previous": null,
|
|
# "next": null
|
|
# },
|
|
# "totalCount": 1
|
|
# }
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v1/wallets/withdrawal
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str [tag]:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['timestamp']: the timestamp of the withdrawal request(mandatory)
|
|
:param str params['nonce']: the nonce of the withdrawal request(mandatory)
|
|
:param str params['network']: network for withdraw(mandatory)
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
# todo check self method properly
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'command': {
|
|
'commandType': 'V1Withdraw',
|
|
'destinationId': address,
|
|
'symbol': currency['id'],
|
|
'quantity': self.currency_to_precision(code, amount),
|
|
},
|
|
}
|
|
networkCode: Str = None
|
|
networkCode, params = self.handle_network_code_and_params(params)
|
|
if networkCode is not None:
|
|
request['network'] = self.network_code_to_id(networkCode)
|
|
else:
|
|
raise ArgumentsRequired(self.id + ' withdraw() requires a network parameter')
|
|
response = self.privatePostV1WalletsWithdrawal(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "code": "00000",
|
|
# "msg": "success",
|
|
# "data": {
|
|
# "orderId":888291686266343424",
|
|
# "clientOrderId":"123"
|
|
# }
|
|
# }
|
|
#
|
|
return self.parse_transaction(response, currency)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# {
|
|
# "custodyTransactionId": "0x791fc85f16a84cbd5250d5517ecad497f564d2e5cc54d31466fe70b952fd58da",
|
|
# "direction": "DEPOSIT",
|
|
# "quantity": "150",
|
|
# "symbol": "USDC",
|
|
# "fee": "0",
|
|
# "memo": "0x34625d5f0b6575503a0669994dea24271bfbd443",
|
|
# "createdAtDateTime": "2025-11-04T14:31:17.000Z",
|
|
# "updatedAtDateTime": "2025-11-04T14:44:17.500Z",
|
|
# "status": "COMPLETE",
|
|
# "statusReason": "OK",
|
|
# "network": "ETH",
|
|
# "transactionDetails": {
|
|
# "address": "0x34625d5f0b6575503a0669994dea24271bfbd443",
|
|
# "blockchainTxId": "0x791fc85f16a84cbd5250d5517ecad497f564d2e5cc54d31466fe70b952fd58da",
|
|
# "swiftUetr": null,
|
|
# "sources": [
|
|
# {
|
|
# "address": "0x2653435d52a5f49551ebb757f25b2c8bb954859b"
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
id = self.safe_string(transaction, 'custodyTransactionId')
|
|
type = self.safe_string(transaction, 'direction')
|
|
timestamp = self.parse8601(self.safe_string(transaction, 'createdAtDateTime'))
|
|
updated = self.parse8601(self.safe_string(transaction, 'updatedAtDateTime'))
|
|
network = self.safe_string(transaction, 'network')
|
|
transactionDetails = self.safe_dict(transaction, 'transactionDetails')
|
|
txid = self.safe_string(transactionDetails, 'blockchainTxId')
|
|
address = self.safe_string(transactionDetails, 'address')
|
|
amount = self.safe_number(transaction, 'quantity')
|
|
currencyId = self.safe_string(transaction, 'symbol')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
status = self.safe_string(transaction, 'status')
|
|
sources = self.safe_list(transactionDetails, 'sources', [])
|
|
source = self.safe_dict(sources, 0, {})
|
|
sourceAddress = self.safe_string(source, 'address')
|
|
fee = {
|
|
'currency': None,
|
|
'cost': None,
|
|
'rate': None,
|
|
}
|
|
feeCost = self.safe_number(transaction, 'fee')
|
|
if feeCost is not None:
|
|
fee['cost'] = feeCost
|
|
fee['currency'] = code
|
|
return {
|
|
'id': id,
|
|
'txid': txid,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'network': self.network_id_to_code(network),
|
|
'addressFrom': sourceAddress,
|
|
'address': address,
|
|
'addressTo': address,
|
|
'amount': amount,
|
|
'type': self.parse_transaction_type(type),
|
|
'currency': code,
|
|
'status': self.parse_transaction_status(status),
|
|
'updated': updated,
|
|
'tagFrom': None,
|
|
'tag': None,
|
|
'tagTo': None,
|
|
'comment': None,
|
|
'internal': None,
|
|
'fee': fee,
|
|
'info': transaction,
|
|
}
|
|
|
|
def parse_transaction_type(self, type):
|
|
types: dict = {
|
|
'DEPOSIT': 'deposit',
|
|
'WITHDRAW': 'withdrawal',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
'COMPLETE': 'ok',
|
|
'FAILED': 'failed',
|
|
'PENDING': 'pending',
|
|
'CANCELLED': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def load_account(self, params={}):
|
|
tradingAccountId: Str = None
|
|
tradingAccountId, params = self.handle_option_and_params(params, 'fetchMyTrades', 'tradingAccountId')
|
|
if tradingAccountId is None:
|
|
response = self.privateGetV1AccountsTradingAccounts(params)
|
|
for i in range(0, len(response)):
|
|
account = response[i]
|
|
name = self.safe_string(account, 'tradingAccountName')
|
|
if name == 'Primary Account':
|
|
tradingAccountId = self.safe_string(account, 'tradingAccountId')
|
|
break
|
|
if tradingAccountId is None:
|
|
raise ArgumentsRequired(self.id + ' loadAccount() requires a tradingAccountId parameter in options["tradingAccountId"] or params["tradingAccountId"], fetchAccounts() was not able to find the Primary account')
|
|
self.options['tradingAccountId'] = tradingAccountId
|
|
return tradingAccountId
|
|
|
|
def fetch_accounts(self, params={}) -> List[Account]:
|
|
"""
|
|
fetch all the accounts associated with a profile
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#tag--trading-accounts
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `account structures <https://docs.ccxt.com/?id=account-structure>` indexed by the account type
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
response = self.privateGetV1AccountsTradingAccounts(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "defaultedMarginUSD": "0.0000",
|
|
# "endCustomerId": "222801149768465",
|
|
# "fullLiquidationMarginUSD": "0.0000",
|
|
# "initialMarginUSD": "0.0000",
|
|
# "isBorrowing": "false",
|
|
# "isConcentrationRiskEnabled": "true",
|
|
# "isDefaulted": "false",
|
|
# "isLending": "false",
|
|
# "isPrimaryAccount": "true",
|
|
# "liquidationMarginUSD": "0.0000",
|
|
# "liquidityAddonUSD": "0.0000",
|
|
# "makerFee": "0.00000000",
|
|
# "marginProfile": {
|
|
# "defaultedMarketRiskMultiplierPct": "50.00",
|
|
# "fullLiquidationMarketRiskMultiplierPct": "75.00",
|
|
# "initialMarketRiskMultiplierPct": "200.00",
|
|
# "liquidationMarketRiskMultiplierPct": "100.00",
|
|
# "warningMarketRiskMultiplierPct": "150.00"
|
|
# },
|
|
# "marketRiskUSD": "0.0000",
|
|
# "maxInitialLeverage": "1",
|
|
# "rateLimitToken": "7fc358f0bad4124528318ff415e24f1ad6e530321827162a5e35d8de8dcfc750",
|
|
# "riskLimitUSD": "0.0000",
|
|
# "takerFee": "0.00000002",
|
|
# "totalBorrowedUSD": "0.0000",
|
|
# "totalCollateralUSD": "0.0000",
|
|
# "totalLiabilitiesUSD": "0.0000",
|
|
# "tradeFeeRate": [
|
|
# {
|
|
# "feeGroupId": "1",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "2",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "3",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "4",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "5",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "6",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "7",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# },
|
|
# {
|
|
# "feeGroupId": "8",
|
|
# "makerFee": "0.00000000",
|
|
# "takerFee": "0.00000000"
|
|
# }
|
|
# ],
|
|
# "tradingAccountDescription": null,
|
|
# "tradingAccountId": "111309424211255",
|
|
# "tradingAccountName": "Primary Account",
|
|
# "warningMarginUSD": "0.0000"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_accounts(response, params)
|
|
|
|
def parse_account(self, account: dict) -> Account:
|
|
return {
|
|
'id': self.safe_string(account, 'tradingAccountId'),
|
|
'type': None,
|
|
'code': None,
|
|
'info': account,
|
|
}
|
|
|
|
def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/wallets/deposit-instructions/crypto/-symbol-
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.network]: network for deposit address
|
|
:returns dict: an `address structure <https://docs.ccxt.com/?id=address-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'symbol': currency['id'],
|
|
}
|
|
response = self.privateGetV1WalletsDepositInstructionsCryptoSymbol(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "network": "ETH",
|
|
# "address": "0xc2fc755082d052bb334763b144851a0031999f33",
|
|
# "symbol": "ETH"
|
|
# }
|
|
# ]
|
|
#
|
|
safeResponse = self.to_array(response)
|
|
length = len(safeResponse)
|
|
data = self.safe_dict(safeResponse, 0, {})
|
|
network = None
|
|
network, params = self.handle_network_code_and_params(params)
|
|
networkDefinedByUser = network is not None
|
|
if (length > 1) or (networkDefinedByUser):
|
|
# some currencies have multiple networks
|
|
if network is None:
|
|
# use default network if not specified and multiple are available
|
|
network = self.default_network_code(code)
|
|
if network is not None:
|
|
# find the entry that matches the network or return first entry if not found and user did not specify a network
|
|
for i in range(0, len(safeResponse)):
|
|
entry = self.safe_dict(safeResponse, i, {})
|
|
networkId = self.safe_string(entry, 'network')
|
|
networkCode = self.network_id_to_code(networkId)
|
|
if network == networkCode:
|
|
data = entry
|
|
break
|
|
if networkDefinedByUser:
|
|
data = {} # return an empty structure if the user-defined network was not found
|
|
return self.parse_deposit_address(data, currency)
|
|
|
|
def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress:
|
|
id = self.safe_string(depositAddress, 'symbol')
|
|
network = self.safe_string(depositAddress, 'network')
|
|
return {
|
|
'info': depositAddress,
|
|
'currency': self.safe_currency_code(id, currency),
|
|
'network': self.network_id_to_code(network),
|
|
'address': self.safe_string(depositAddress, 'address'),
|
|
'tag': None,
|
|
}
|
|
|
|
def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for 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/#get-/v1/accounts/asset
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/accounts/asset/-symbol-
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['tradingAccountId']: the trading account id(mandatory parameter)
|
|
:param str [params.code]: unified currency code, default is None
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
response = None
|
|
code = self.safe_string(params, 'code')
|
|
if code is not None:
|
|
request['symbol'] = self.currency(code)['id']
|
|
response = self.privateGetV1AccountsAssetSymbol(self.extend(request, params))
|
|
return self.parse_balance_for_single_currency(response, code)
|
|
else:
|
|
response = self.privateGetV1AccountsAsset(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "assetId": "10",
|
|
# "assetSymbol": "AAVE",
|
|
# "availableQuantity": "10000000.00000000",
|
|
# "borrowedQuantity": "0.00000000",
|
|
# "loanedQuantity": "0.00000000",
|
|
# "lockedQuantity": "0.00000000",
|
|
# "publishedAtTimestamp": "1747942728870",
|
|
# "tradingAccountId": "111309424211255",
|
|
# "updatedAtDatetime": "2025-05-13T11:33:08.801Z",
|
|
# "updatedAtTimestamp": "1747135988801"
|
|
# }, ...
|
|
# ]
|
|
#
|
|
return self.parse_balance(response)
|
|
|
|
def parse_balance_for_single_currency(self, response, code: Str) -> Balances:
|
|
result: dict = {'info': response}
|
|
account = self.account()
|
|
account['free'] = self.safe_string(response, 'availableQuantity')
|
|
account['used'] = self.safe_string(response, 'lockedQuantity')
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
result: dict = {
|
|
'info': response,
|
|
}
|
|
for i in range(0, len(response)):
|
|
balance = response[i]
|
|
symbol = self.safe_string(balance, 'assetSymbol')
|
|
code = self.safe_currency_code(symbol)
|
|
account = self.account()
|
|
account['total'] = self.safe_string(balance, 'availableQuantity')
|
|
account['used'] = self.safe_string(balance, 'lockedQuantity')
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/derivatives-positions
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['tradingAccountId']: the trading account id
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
response = self.privateGetV1DerivativesPositions(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "tradingAccountId": "111000000000001",
|
|
# "symbol": "BTC-USDC-PERP",
|
|
# "side": "BUY",
|
|
# "quantity": "1.00000000",
|
|
# "notional": "1.0000",
|
|
# "entryNotional": "1.0000",
|
|
# "mtmPnl": "1.0000",
|
|
# "reportedMtmPnl": "1.0000",
|
|
# "reportedFundingPnl": "1.0000",
|
|
# "realizedPnl": "1.0000",
|
|
# "settlementAssetSymbol": "USDC",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "updatedAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "updatedAtTimestamp": "1621490985000"
|
|
# }
|
|
# ]
|
|
#
|
|
results = self.parse_positions(response, symbols)
|
|
return self.filter_by_array_positions(results, 'symbol', symbols, False)
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# [
|
|
# {
|
|
# "tradingAccountId": "111000000000001",
|
|
# "symbol": "BTC-USDC-PERP",
|
|
# "side": "BUY",
|
|
# "quantity": "1.00000000",
|
|
# "notional": "1.0000",
|
|
# "entryNotional": "1.0000",
|
|
# "mtmPnl": "1.0000",
|
|
# "reportedMtmPnl": "1.0000",
|
|
# "reportedFundingPnl": "1.0000",
|
|
# "realizedPnl": "1.0000",
|
|
# "settlementAssetSymbol": "USDC",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "updatedAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "updatedAtTimestamp": "1621490985000"
|
|
# }
|
|
# ]
|
|
#
|
|
market = self.safe_market(self.safe_string(position, 'symbol'), market)
|
|
symbol = market['symbol']
|
|
timestamp = self.safe_integer(position, 'createdAtTimestamp')
|
|
side = self.safe_string(position, 'side')
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastUpdateTimestamp': self.safe_integer(position, 'updatedAtTimestamp'),
|
|
'hedged': None,
|
|
'side': self.parse_position_side(side),
|
|
'contracts': self.safe_number(position, 'quantity'),
|
|
'contractSize': None,
|
|
'entryPrice': None,
|
|
'markPrice': None,
|
|
'lastPrice': None,
|
|
'notional': self.safe_number(position, 'notional'),
|
|
'leverage': None,
|
|
'collateral': None,
|
|
'initialMargin': None,
|
|
'initialMarginPercentage': None,
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'unrealizedPnl': None,
|
|
'liquidationPrice': None,
|
|
'marginMode': None,
|
|
'marginRatio': None,
|
|
'percentage': None,
|
|
'stopLossPrice': None,
|
|
'takeProfitPrice': None,
|
|
})
|
|
|
|
def parse_position_side(self, side: Str):
|
|
sides: dict = {
|
|
'BUY': 'long',
|
|
'SELL': 'short',
|
|
}
|
|
return self.safe_string(sides, side, side)
|
|
|
|
def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]:
|
|
"""
|
|
fetch a history of internal transfers made on an account
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/transfer
|
|
|
|
:param str code: unified currency code of the currency transferred
|
|
:param int [since]: the earliest time in ms to fetch transfers for
|
|
:param int [limit]: the maximum number of transfer structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int params['until']: the latest time in ms to fetch transfers for(default time now)
|
|
:param str params['tradingAccountId']: the trading account id
|
|
:returns dict[]: a list of `transfer structures <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
maxLimit = 100
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate')
|
|
if paginate:
|
|
params = self.handle_pagination_params('fetchTransfers', since, params)
|
|
return self.fetch_paginated_call_dynamic('fetchTransfers', code, since, limit, params, maxLimit)
|
|
request: dict = {
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
currency: Currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['assetSymbol'] = currency['id']
|
|
until = self.safe_integer(params, 'until')
|
|
if (since is None) and (until is None):
|
|
# since and until are mandatory for self endpoint, set until to now if both are None
|
|
now = self.milliseconds()
|
|
params = self.extend(params, {'until': now})
|
|
params = self.handle_since_and_until(since, params)
|
|
if limit is not None:
|
|
request['_pageSize'] = self.get_closest_limit(limit)
|
|
response = self.privateGetV1HistoryTransfer(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "requestId": "1",
|
|
# "toTradingAccountId": "111000000000001",
|
|
# "fromTradingAccountId": "121000000000001",
|
|
# "assetSymbol": "BTC",
|
|
# "quantity": "1.00000000",
|
|
# "status": "CLOSED",
|
|
# "statusReasonCode": "6002",
|
|
# "statusReason": "Executed",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_transfers(response, currency, since, limit)
|
|
|
|
def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
transfer currency internally between wallets on the same account
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#post-/v1/command-commandType-V1TransferAsset
|
|
|
|
:param str code: unified currency codeåå
|
|
:param float amount: amount to transfer
|
|
:param str fromAccount: account ID to transfer from
|
|
:param str toAccount: account ID to transfer to
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transfer structure <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
# todo check self method properly
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'commandType': 'V2TransferAsset',
|
|
'assetSymbol': currency['id'],
|
|
'quantity': self.currency_to_precision(code, amount),
|
|
'fromTradingAccountId': fromAccount,
|
|
'toTradingAccountId': toAccount,
|
|
}
|
|
response = self.privatePostV2Command(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "message": "Command acknowledged - TransferAsset",
|
|
# "requestId": "633909659774222336"
|
|
# }
|
|
#
|
|
transferOptions = self.safe_dict(self.options, 'transfer', {})
|
|
fillResponseFromRequest = self.safe_bool(transferOptions, 'fillResponseFromRequest', True)
|
|
transfer = self.parse_transfer(response, currency)
|
|
if fillResponseFromRequest:
|
|
transfer['fromAccount'] = fromAccount
|
|
transfer['toAccount'] = toAccount
|
|
transfer['amount'] = amount
|
|
transfer['currency'] = code
|
|
return transfer
|
|
|
|
def parse_transfer(self, transfer, currency: Currency = None):
|
|
#
|
|
# fetchTransfers
|
|
# {
|
|
# "requestId": "1",
|
|
# "toTradingAccountId": "111000000000001",
|
|
# "fromTradingAccountId": "121000000000001",
|
|
# "assetSymbol": "BTC",
|
|
# "quantity": "1.00000000",
|
|
# "status": "CLOSED",
|
|
# "statusReasonCode": "6002",
|
|
# "statusReason": "Executed",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z"
|
|
# }
|
|
#
|
|
# transfer
|
|
# {
|
|
# "message": "Command acknowledged - TransferAsset",
|
|
# "requestId": "633909659774222336"
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer(transfer, 'createdAtTimestamp')
|
|
currencyId = self.safe_string(transfer, 'assetSymbol')
|
|
status = self.safe_string(transfer, 'status')
|
|
if status is None:
|
|
status = self.safe_string(transfer, 'message')
|
|
return {
|
|
'id': self.safe_string(transfer, 'requestId'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'currency': self.safe_currency_code(currencyId, currency),
|
|
'amount': self.safe_number(transfer, 'quantity'),
|
|
'fromAccount': self.safe_string(transfer, 'fromTradingAccountId'),
|
|
'toAccount': self.safe_string(transfer, 'toTradingAccountId'),
|
|
'status': self.parse_transfer_status(status),
|
|
'info': transfer,
|
|
}
|
|
|
|
def parse_transfer_status(self, status):
|
|
statuses: dict = {
|
|
'CLOSED': 'ok',
|
|
'OPEN': 'pending',
|
|
'REJECTED': 'failed',
|
|
'Command acknowledged - TransferAsset': 'ok',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
retrieves a history of a currencies borrow interest rate at specific time slots
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/history/borrow-interest
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: timestamp for the earliest borrow rate
|
|
:param int [limit]: the maximum number of `borrow rate structures <https://docs.ccxt.com/?id=borrow-rate-structure>` to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int params['until']: the latest time in ms to fetch entries for
|
|
:param str params['tradingAccountId']: the trading account id
|
|
:returns dict[]: an array of `borrow rate structures <https://docs.ccxt.com/?id=borrow-rate-structure>`
|
|
"""
|
|
[self.load_markets(), self.handle_token()]
|
|
tradingAccountId = self.load_account(params)
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'assetSymbol': currency['id'],
|
|
'tradingAccountId': tradingAccountId,
|
|
}
|
|
now = self.milliseconds()
|
|
startTimestamp = since
|
|
request, params = self.handle_until_option('createdAtDatetime[lte]', request, params)
|
|
until = self.safe_integer(request, 'createdAtDatetime[lte]')
|
|
# current endpoint requires both since and until parameters
|
|
if startTimestamp is None:
|
|
startTimestamp = now - 1000 * 60 * 60 * 24 * 90 # Only the last 90 days of data is available for querying
|
|
if until is None:
|
|
until = now
|
|
request['createdAtDatetime[gte]'] = self.iso8601(startTimestamp)
|
|
request['createdAtDatetime[lte]'] = self.iso8601(until)
|
|
response = self.privateGetV1HistoryBorrowInterest(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "assetId": "1",
|
|
# "assetSymbol": "BTC",
|
|
# "borrowedQuantity": "1.00000000",
|
|
# "totalBorrowedQuantity": "1.00000000",
|
|
# "createdAtDatetime": "2020-08-21T08:00:00.000Z",
|
|
# "createdAtTimestamp": "1621490985000"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_borrow_rate_history(response, code, since, limit)
|
|
|
|
def parse_borrow_rate(self, info, currency: Currency = None):
|
|
#
|
|
# {
|
|
# "assetId": "1",
|
|
# "assetSymbol": "BTC",
|
|
# "borrowedQuantity": "1.00000000",
|
|
# "totalBorrowedQuantity": "1.00000000",
|
|
# "createdAtDatetime": "2020-08-21T08:00:00.000Z",
|
|
# "createdAtTimestamp": "1621490985000"
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer(info, 'createdAtTimestamp')
|
|
currencyId = self.safe_string(info, 'assetSymbol')
|
|
return {
|
|
'currency': self.safe_currency_code(currencyId, currency),
|
|
'rate': self.safe_number(info, 'borrowedQuantity'),
|
|
'period': 86400000,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'info': info,
|
|
}
|
|
|
|
def get_timestamp(self):
|
|
return self.milliseconds() - self.options['timeDifference']
|
|
|
|
def fetch_open_interest(self, symbol: str, params={}) -> OpenInterest:
|
|
"""
|
|
fetches the open interest of a specific market
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#get-/v1/markets/-symbol-/tick
|
|
|
|
:param str symbol: unified symbol of the market to fetch the open interest for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `open interest structure <https://docs.ccxt.com/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
response = self.publicGetV1MarketsSymbolTick(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "high": "1.00000000",
|
|
# "low": "1.00000000",
|
|
# "bestBid": "1.00000000",
|
|
# "bidVolume": "1.00000000",
|
|
# "bestAsk": "1.00000000",
|
|
# "askVolume": "1.00000000",
|
|
# "vwap": "1.00000000",
|
|
# "open": "1.00000000",
|
|
# "close": "1.00000000",
|
|
# "last": "1.00000000",
|
|
# "change": "1.00000000",
|
|
# "percentage": "1.00000000",
|
|
# "average": "1.00000000",
|
|
# "baseVolume": "1.00000000",
|
|
# "quoteVolume": "1.00000000",
|
|
# "bancorPrice": "1.00000000",
|
|
# "markPrice": "19999.00",
|
|
# "fundingRate": "0.01",
|
|
# "openInterest": "100000.32452",
|
|
# "lastTradeDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "lastTradeTimestamp": "1621490985000",
|
|
# "lastTradeQuantity": "1.00000000",
|
|
# "ammData": [
|
|
# {
|
|
# "feeTierId": "1",
|
|
# "bidSpreadFee": "0.00040000",
|
|
# "askSpreadFee": "0.00040000",
|
|
# "baseReservesQuantity": "245.56257825",
|
|
# "quoteReservesQuantity": "3424383.3629",
|
|
# "currentPrice": "16856.0000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
return self.parse_open_interest(response, market)
|
|
|
|
def parse_open_interest(self, interest, market: Market = None):
|
|
#
|
|
# {
|
|
# "createdAtDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "createdAtTimestamp": "1621490985000",
|
|
# "high": "1.00000000",
|
|
# "low": "1.00000000",
|
|
# "bestBid": "1.00000000",
|
|
# "bidVolume": "1.00000000",
|
|
# "bestAsk": "1.00000000",
|
|
# "askVolume": "1.00000000",
|
|
# "vwap": "1.00000000",
|
|
# "open": "1.00000000",
|
|
# "close": "1.00000000",
|
|
# "last": "1.00000000",
|
|
# "change": "1.00000000",
|
|
# "percentage": "1.00000000",
|
|
# "average": "1.00000000",
|
|
# "baseVolume": "1.00000000",
|
|
# "quoteVolume": "1.00000000",
|
|
# "bancorPrice": "1.00000000",
|
|
# "markPrice": "19999.00",
|
|
# "fundingRate": "0.01",
|
|
# "openInterest": "100000.32452",
|
|
# "lastTradeDatetime": "2021-05-20T01:01:01.000Z",
|
|
# "lastTradeTimestamp": "1621490985000",
|
|
# "lastTradeQuantity": "1.00000000",
|
|
# "ammData": [
|
|
# {
|
|
# "feeTierId": "1",
|
|
# "bidSpreadFee": "0.00040000",
|
|
# "askSpreadFee": "0.00040000",
|
|
# "baseReservesQuantity": "245.56257825",
|
|
# "quoteReservesQuantity": "3424383.3629",
|
|
# "currentPrice": "16856.0000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
openInterest = self.safe_string(interest, 'openInterest')
|
|
return self.safe_open_interest({
|
|
'info': interest,
|
|
'symbol': self.safe_string(market, 'symbol'),
|
|
'openInterestAmount': openInterest,
|
|
'openInterestValue': None,
|
|
'timestamp': self.safe_string(interest, 'createdAtTimestamp'),
|
|
'datetime': self.safe_string(interest, 'createdAtDatetime'),
|
|
'baseVolume': openInterest,
|
|
'quoteVolume': None,
|
|
}, market)
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
request = self.omit(params, self.extract_params(path))
|
|
endpoint = '/' + self.implode_params(path, params)
|
|
url = self.urls['api'][api] + endpoint
|
|
if api == 'private':
|
|
self.check_required_credentials()
|
|
nonce = str(self.microseconds())
|
|
timestamp = str(self.get_timestamp())
|
|
if method == 'GET':
|
|
payload = timestamp + nonce + method + '/trading-api/' + path
|
|
signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex')
|
|
headers = {
|
|
'BX-TIMESTAMP': timestamp,
|
|
'BX-NONCE': nonce,
|
|
'BX-SIGNATURE': signature,
|
|
}
|
|
elif method == 'POST':
|
|
body = self.json(params)
|
|
payload = timestamp + nonce + method + '/trading-api/' + path + body
|
|
digest = self.hash(self.encode(payload), 'sha256', 'hex')
|
|
signature = self.hmac(self.encode(digest), self.encode(self.secret), hashlib.sha256, 'hex')
|
|
headers = {
|
|
'BX-TIMESTAMP': timestamp,
|
|
'BX-NONCE': nonce,
|
|
'BX-SIGNATURE': signature,
|
|
'Content-Type': 'application/json',
|
|
}
|
|
headers['Content-Type'] = 'application/json'
|
|
rateLimitToken = self.safe_string(request, 'rateLimitToken')
|
|
if rateLimitToken is not None:
|
|
headers['BX-RATE-LIMIT-TOKEN'] = rateLimitToken
|
|
if path == 'v1/users/hmac/login':
|
|
headers['BX-PUBLIC-KEY'] = self.apiKey
|
|
else:
|
|
token = self.token
|
|
if (token is None):
|
|
raise AuthenticationError(self.id + ' requires a token, please call signIn() first')
|
|
headers['Authorization'] = 'Bearer ' + token
|
|
# headers['BX-NONCE-WINDOW-ENABLED'] = 'false' # default is False
|
|
if method == 'GET':
|
|
query = self.urlencode(request)
|
|
if len(query):
|
|
url += '?' + query
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def sign_in(self, params={}):
|
|
"""
|
|
sign in, must be called prior to using other authenticated methods
|
|
|
|
https://api.exchange.bullish.com/docs/api/rest/trading-api/v2/#overview--add-authenticated-request-header
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns: response from exchange
|
|
"""
|
|
response = self.privateGetV1UsersHmacLogin(params)
|
|
#
|
|
# {
|
|
# "authorizer": "113363EFA2CA00007368524E02000000",
|
|
# "ownerAuthorizer": "113363EFA2CA00007368524E02000000",
|
|
# "token": "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJiMXgtYXV0aC1zZXJ2aWNlIiwic3ViIjoiNDY0OTc4MzAiLCJleHAiOjE3NDczMzgzNDMsIlNUQUdFIjoiQVVUSEVOVElDQVRFRF9XSVRIX0JMT0NLQ0hBSU4ifQ.5FSyrihzc1wsJqAY8pVX36Y4ZXg3HopLJypPEbHg5bBK8FbL_oLxkj6zM_iOYL2a1x6-ICG0pQjr8hF_k8Yg-w"
|
|
# }
|
|
#
|
|
token = self.safe_string(response, 'token')
|
|
authorizer = self.safe_string(response, 'authorizer')
|
|
self.options['authorizer'] = authorizer
|
|
self.token = token
|
|
self.options['tokenExpires'] = self.sum(self.milliseconds(), 1000 * 60 * 60 * 24) # token expires in 24 hours
|
|
return token
|
|
|
|
def handle_token(self, params={}):
|
|
now = self.milliseconds()
|
|
token = self.token
|
|
tokenExpires = self.safe_integer(self.options, 'tokenExpires')
|
|
if (token is None) or (tokenExpires is None) or (now > tokenExpires):
|
|
return self.sign_in()
|
|
else:
|
|
return self.token
|
|
|
|
def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if response is None:
|
|
return None # fallback to default error handler
|
|
#
|
|
# {
|
|
# "type": "HttpInvalidParameterException",
|
|
# "message": "HTTP_INVALID_PARAMETER: '100m' is not a valid time bucket"
|
|
# }
|
|
#
|
|
# {
|
|
# "message": "Order size outside valid range",
|
|
# "raw": null,
|
|
# "errorCode": 6023,
|
|
# "errorCodeName": "ORDER_SIZE_OUTSIDE_VALID_RANGE"
|
|
# }
|
|
#
|
|
code = self.safe_string(response, 'errorCode')
|
|
type = self.safe_string(response, 'type')
|
|
if (code is not None and code != '0' and code != '1001') or (type is not None and type == 'HttpInvalidParameterException'):
|
|
message = ''
|
|
errorCodeName = self.safe_string(response, 'errorCodeName')
|
|
if errorCodeName is not None:
|
|
message = errorCodeName
|
|
else:
|
|
message = type
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
raise ExchangeError(feedback) # unknown message
|
|
return None
|