# -*- 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.blofin import ImplicitAPI import hashlib from ccxt.base.types import Any, Balances, Currency, Int, LedgerEntry, Leverage, Leverages, MarginMode, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, TransferEntry from typing import List from ccxt.base.errors import ExchangeError from ccxt.base.errors import AuthenticationError from ccxt.base.errors import ArgumentsRequired from ccxt.base.errors import BadRequest from ccxt.base.errors import InsufficientFunds from ccxt.base.errors import InvalidOrder from ccxt.base.errors import RateLimitExceeded from ccxt.base.errors import ExchangeNotAvailable from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.precise import Precise class blofin(Exchange, ImplicitAPI): def describe(self) -> Any: return self.deep_extend(super(blofin, self).describe(), { 'id': 'blofin', 'name': 'BloFin', 'countries': ['US'], 'version': 'v1', 'rateLimit': 100, 'pro': True, 'has': { 'CORS': None, 'spot': False, 'margin': False, 'swap': True, 'future': False, 'option': False, 'addMargin': False, 'borrowMargin': False, 'cancelAllOrders': False, 'cancelOrder': True, 'cancelOrders': True, 'closeAllPositions': False, 'closePosition': True, 'createDepositAddress': False, 'createMarketBuyOrderWithCost': False, 'createMarketSellOrderWithCost': False, 'createOrder': True, 'createOrders': True, 'createOrderWithTakeProfitAndStopLoss': True, 'createPostOnlyOrder': False, 'createReduceOnlyOrder': False, 'createStopLimitOrder': False, 'createStopLossOrder': True, 'createStopMarketOrder': False, 'createStopOrder': False, 'createTakeProfitOrder': True, 'createTriggerOrder': True, 'editOrder': False, 'fetchAccounts': False, 'fetchBalance': True, 'fetchBidsAsks': None, 'fetchBorrowInterest': False, 'fetchBorrowRateHistories': False, 'fetchBorrowRateHistory': False, 'fetchCanceledOrders': False, 'fetchClosedOrder': False, 'fetchClosedOrders': True, 'fetchCrossBorrowRate': False, 'fetchCrossBorrowRates': False, 'fetchCurrencies': False, 'fetchDeposit': False, 'fetchDepositAddress': False, 'fetchDepositAddresses': False, 'fetchDepositAddressesByNetwork': False, 'fetchDeposits': True, 'fetchDepositsWithdrawals': False, 'fetchDepositWithdrawFee': 'emulated', 'fetchDepositWithdrawFees': False, 'fetchFundingHistory': True, 'fetchFundingRate': True, 'fetchFundingRateHistory': True, 'fetchFundingRates': False, 'fetchGreeks': False, 'fetchIndexOHLCV': False, 'fetchIsolatedBorrowRate': False, 'fetchIsolatedBorrowRates': False, 'fetchL3OrderBook': False, 'fetchLedger': True, 'fetchLedgerEntry': None, 'fetchLeverage': True, 'fetchLeverages': True, 'fetchLeverageTiers': False, 'fetchMarginMode': True, 'fetchMarginModes': False, 'fetchMarketLeverageTiers': False, 'fetchMarkets': True, 'fetchMarkOHLCV': False, 'fetchMySettlementHistory': False, 'fetchMyTrades': True, 'fetchOHLCV': True, 'fetchOpenInterest': False, 'fetchOpenInterestHistory': False, 'fetchOpenOrder': None, 'fetchOpenOrders': True, 'fetchOrder': None, 'fetchOrderBook': True, 'fetchOrderBooks': False, 'fetchOrders': False, 'fetchOrderTrades': True, 'fetchPosition': True, 'fetchPositionMode': True, 'fetchPositions': True, 'fetchPositionsForSymbol': False, 'fetchPositionsRisk': False, 'fetchPremiumIndexOHLCV': False, 'fetchSettlementHistory': False, 'fetchStatus': False, 'fetchTicker': True, 'fetchTickers': True, 'fetchTime': False, 'fetchTrades': True, 'fetchTradingFee': False, 'fetchTradingFees': False, 'fetchTradingLimits': False, 'fetchTransactionFee': False, 'fetchTransactionFees': False, 'fetchTransactions': False, 'fetchTransfer': False, 'fetchTransfers': False, 'fetchUnderlyingAssets': False, 'fetchVolatilityHistory': False, 'fetchWithdrawal': False, 'fetchWithdrawals': True, 'fetchWithdrawalWhitelist': False, 'reduceMargin': False, 'repayCrossMargin': False, 'setLeverage': True, 'setMargin': False, 'setMarginMode': True, 'setPositionMode': True, 'signIn': False, 'transfer': True, 'withdraw': False, }, 'timeframes': { '1m': '1m', '3m': '3m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1H', '2h': '2H', '4h': '4H', '6h': '6H', '8h': '8H', '12h': '12H', '1d': '1D', '3d': '3D', '1w': '1W', '1M': '1M', }, 'hostname': 'www.blofin.com', 'urls': { 'logo': 'https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b', 'api': { 'rest': 'https://openapi.blofin.com', }, 'test': { 'rest': 'https://demo-trading-openapi.blofin.com', }, 'referral': { 'url': 'https://blofin.com/register?referral_code=f79EsS', 'discount': 0.05, }, 'www': 'https://www.blofin.com', 'doc': 'https://blofin.com/docs', }, 'api': { 'public': { 'get': { 'market/instruments': 1, 'market/tickers': 1, 'market/books': 1, 'market/trades': 1, 'market/candles': 1, 'market/mark-price': 1, 'market/funding-rate': 1, 'market/funding-rate-history': 1, }, }, 'private': { 'get': { 'asset/balances': 1, 'trade/orders-pending': 1, 'trade/fills-history': 1, 'asset/deposit-history': 1, 'asset/withdrawal-history': 1, 'asset/bills': 1, 'account/balance': 1, 'account/positions': 1, 'account/leverage-info': 1, 'account/margin-mode': 1, 'account/position-mode': 1, 'account/batch-leverage-info': 1, 'trade/orders-tpsl-pending': 1, 'trade/orders-algo-pending': 1, 'trade/orders-history': 1, 'trade/orders-tpsl-history': 1, 'trade/orders-algo-history': 1, # todo new 'trade/order/price-range': 1, 'user/query-apikey': 1, 'affiliate/basic': 1, 'copytrading/instruments': 1, 'copytrading/account/balance': 1, 'copytrading/account/positions-by-order': 1, 'copytrading/account/positions-details-by-order': 1, 'copytrading/account/positions-by-contract': 1, 'copytrading/account/position-mode': 1, 'copytrading/account/leverage-info': 1, 'copytrading/trade/orders-pending': 1, 'copytrading/trade/pending-tpsl-by-contract': 1, 'copytrading/trade/position-history-by-order': 1, 'copytrading/trade/orders-history': 1, 'copytrading/trade/pending-tpsl-by-order': 1, }, 'post': { 'account/set-margin-mode': 1, 'account/set-position-mode': 1, 'trade/order': 1, 'trade/order-algo': 1, 'trade/cancel-order': 1, 'trade/cancel-algo': 1, 'account/set-leverage': 1, 'trade/batch-orders': 1, 'trade/order-tpsl': 1, 'trade/cancel-batch-orders': 1, 'trade/cancel-tpsl': 1, 'trade/close-position': 1, 'asset/transfer': 1, 'copytrading/account/set-position-mode': 1, 'copytrading/account/set-leverage': 1, 'copytrading/trade/place-order': 1, 'copytrading/trade/cancel-order': 1, 'copytrading/trade/place-tpsl-by-contract': 1, 'copytrading/trade/cancel-tpsl-by-contract': 1, 'copytrading/trade/place-tpsl-by-order': 1, 'copytrading/trade/cancel-tpsl-by-order': 1, 'copytrading/trade/close-position-by-order': 1, 'copytrading/trade/close-position-by-contract': 1, }, }, }, 'fees': { 'swap': { 'taker': self.parse_number('0.00060'), 'maker': self.parse_number('0.00020'), }, }, 'requiredCredentials': { 'apiKey': True, 'secret': True, 'password': True, }, 'features': { 'default': { 'sandbox': False, 'createOrder': { 'timeInForce': { 'IOC': True, 'FOK': True, 'PO': True, 'GTD': False, }, 'leverage': False, 'marketBuyRequiresPrice': False, 'marketBuyByCost': False, 'selfTradePrevention': False, 'trailing': False, 'iceberg': False, }, 'createOrders': { 'max': 10, }, 'fetchMyTrades': { 'marginMode': False, 'limit': 100, 'daysBack': 100000, 'untilDays': 100000, 'symbolRequired': False, }, 'fetchOrder': None, 'fetchOpenOrders': { 'marginMode': False, 'limit': 100, 'trigger': True, 'trailing': False, 'symbolRequired': False, }, 'fetchOrders': None, 'fetchClosedOrders': { 'marginMode': False, 'limit': 1000, 'daysBack': 100000, 'daysBackCanceled': 1, 'untilDays': 100000, 'trigger': True, 'trailing': False, 'symbolRequired': False, }, 'fetchOHLCV': { 'limit': 1440, }, }, 'spot': { 'extends': 'default', 'createOrder': { 'marginMode': False, 'triggerPrice': False, 'triggerPriceType': None, 'triggerDirection': False, 'stopLossPrice': False, 'takeProfitPrice': False, 'attachedStopLossTakeProfit': None, 'hedged': False, }, }, 'forDerivatives': { 'extends': 'default', 'createOrder': { 'marginMode': True, 'triggerPrice': False, # todo 'triggerPriceType': None, 'triggerDirection': False, 'stopLossPrice': True, 'takeProfitPrice': True, 'attachedStopLossTakeProfit': { 'triggerPriceType': None, 'price': True, }, 'hedged': True, }, }, 'swap': { 'linear': { 'extends': 'forDerivatives', }, 'inverse': None, }, 'future': { 'linear': None, 'inverse': None, }, }, 'exceptions': { 'exact': { '400': BadRequest, # Body can not be empty '401': AuthenticationError, # Invalid signature '500': ExchangeError, # Internal Server Error '404': BadRequest, # not found '405': BadRequest, # Method Not Allowed '406': BadRequest, # Not Acceptable '429': RateLimitExceeded, # Too Many Requests '152001': BadRequest, # Parameter {} cannot be empty '152002': BadRequest, # Parameter {} error '152003': BadRequest, # Either parameter {} or {} is required '152004': BadRequest, # JSON syntax error '152005': BadRequest, # Parameter error: wrong or empty '152006': InvalidOrder, # Batch orders can be placed for up to 20 at once '152007': InvalidOrder, # Batch orders can only be placed with the same instId and marginMode '152008': InvalidOrder, # Only the same field is allowed for bulk cancellation of orders, orderId is preferred '152009': InvalidOrder, # {} must be a combination of numbers, letters, or underscores, and the maximum length of characters is 32 '150003': InvalidOrder, # clientId already exist '150004': InvalidOrder, # Insufficient balance. please adjust the amount and try again '542': InvalidOrder, # Exceeded the maximum order size limit '102002': InvalidOrder, # Duplicate customized order ID '102005': InvalidOrder, # Position had been closed '102014': InvalidOrder, # Limit order exceeds maximum order size limit '102015': InvalidOrder, # Market order exceeds maximum order size limit '102022': InvalidOrder, # Failed to place order. You don’t have any positions of self contract. Turn off Reduce-only to continue. '102037': InvalidOrder, # TP trigger price should be higher than the latest trading price '102038': InvalidOrder, # SL trigger price should be lower than the latest trading price '102039': InvalidOrder, # TP trigger price should be lower than the latest trading price '102040': InvalidOrder, # SL trigger price should be higher than the latest trading price '102047': InvalidOrder, # Stop loss trigger price should be higher than the order price '102048': InvalidOrder, # stop loss trigger price must be higher than the best bid price '102049': InvalidOrder, # Take profit trigger price should be lower than the order price '102050': InvalidOrder, # stop loss trigger price must be lower than the best ask price '102051': InvalidOrder, # stop loss trigger price should be lower than the order price '102052': InvalidOrder, # take profit trigger price should be higher than the order price '102053': InvalidOrder, # take profit trigger price should be lower than the best bid price '102054': InvalidOrder, # take profit trigger price should be higher than the best ask price '102055': InvalidOrder, # stop loss trigger price should be lower than the best ask price '102064': BadRequest, # Buy price is not within the price limit(Minimum: 310.40; Maximum:1,629.40) '102065': BadRequest, # Sell price is not within the price limit '102068': BadRequest, # Cancel failed order has been filled, triggered, canceled or does not exist '103013': ExchangeError, # Internal error; unable to process your request. Please try again. 'Order failed. Insufficient USDT margin in account': InsufficientFunds, # Insufficient USDT margin in account }, 'broad': { 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} }, }, 'httpExceptions': { '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 }, 'precisionMode': TICK_SIZE, 'options': { 'brokerId': 'ec6dd3a7dd982d0b', 'accountsByType': { 'swap': 'futures', 'funding': 'funding', 'future': 'futures', 'copy_trading': 'copy_trading', 'earn': 'earn', 'spot': 'spot', }, 'accountsById': { 'funding': 'funding', 'futures': 'swap', 'copy_trading': 'copy_trading', 'earn': 'earn', 'spot': 'spot', }, 'defaultNetwork': 'ERC20', 'defaultNetworks': { 'ETH': 'ERC20', 'BTC': 'BTC', 'USDT': 'TRC20', }, 'networks': { 'BTC': 'Bitcoin', 'BEP20': 'BSC', 'ERC20': 'ERC20', 'TRC20': 'TRC20', }, 'fetchOpenInterestHistory': { 'timeframes': { '5m': '5m', '1h': '1H', '8h': '8H', '1d': '1D', '5M': '5m', '1H': '1H', '8H': '8H', '1D': '1D', }, }, 'fetchOHLCV': { # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles 'timezone': 'UTC', # UTC, HK }, 'fetchPositions': { 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory }, 'createOrder': 'privatePostTradeOrder', # or 'privatePostTradeOrderTpsl' 'createMarketBuyOrderRequiresPrice': False, 'fetchMarkets': ['swap'], 'defaultType': 'swap', 'fetchLedger': { 'method': 'privateGetAssetBills', }, 'fetchOpenOrders': { 'method': 'privateGetTradeOrdersPending', }, 'cancelOrders': { 'method': 'privatePostTradeCancelBatchOrders', }, 'fetchCanceledOrders': { 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory }, 'fetchClosedOrders': { 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersTpslHistory }, 'withdraw': { # a funding password credential is required by the exchange for the # withdraw call(not to be confused with the api password credential) 'password': None, 'pwd': None, # password or pwd both work }, 'exchangeType': { 'spot': 'SPOT', 'swap': 'SWAP', 'SPOT': 'SPOT', 'SWAP': 'SWAP', }, }, }) def fetch_markets(self, params={}) -> List[Market]: """ retrieves data on all markets for blofin https://blofin.com/docs#get-instruments :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ response = self.publicGetMarketInstruments(params) data = self.safe_list(response, 'data', []) return self.parse_markets(data) def parse_market(self, market: dict) -> Market: id = self.safe_string(market, 'instId') type = self.safe_string_lower(market, 'instType') spot = (type == 'spot') future = (type == 'future') swap = (type == 'swap') option = (type == 'option') contract = swap or future baseId = self.safe_string(market, 'baseCurrency') quoteId = self.safe_string(market, 'quoteCurrency') settleId = self.safe_string(market, 'quoteCurrency') settle = self.safe_currency_code(settleId) base = self.safe_currency_code(baseId) quote = self.safe_currency_code(quoteId) symbol = base + '/' + quote if swap: symbol = symbol + ':' + settle expiry = None strikePrice = None optionType = None tickSize = self.safe_string(market, 'tickSize') fees = self.safe_dict_2(self.fees, type, 'trading', {}) taker = self.safe_number(fees, 'taker') maker = self.safe_number(fees, 'maker') maxLeverage = self.safe_string(market, 'maxLeverage', '100') maxLeverage = Precise.string_max(maxLeverage, '1') isActive = (self.safe_string(market, 'state') == 'live') return self.safe_market_structure({ 'id': id, 'symbol': symbol, 'base': base, 'quote': quote, 'baseId': baseId, 'quoteId': quoteId, 'settle': settle, 'settleId': settleId, 'type': type, 'spot': spot, 'option': option, 'margin': spot and (Precise.string_gt(maxLeverage, '1')), 'swap': swap, 'future': future, 'active': isActive, 'taker': taker, 'maker': maker, 'contract': contract, 'linear': (quoteId == settleId) if contract else None, 'inverse': (baseId == settleId) if contract else None, 'contractSize': self.safe_number(market, 'contractValue') if contract else None, 'expiry': expiry, 'expiryDatetime': expiry, 'strike': strikePrice, 'optionType': optionType, 'created': self.safe_integer(market, 'listTime'), 'precision': { 'amount': self.safe_number(market, 'lotSize'), 'price': self.parse_number(tickSize), }, 'limits': { 'leverage': { 'min': self.parse_number('1'), 'max': self.parse_number(maxLeverage), }, 'amount': { 'min': self.safe_number(market, 'minSize'), 'max': None, }, 'price': { 'min': None, 'max': None, }, 'cost': { 'min': None, 'max': None, }, }, 'info': market, }) 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://blofin.com/docs#get-order-book :param str symbol: unified symbol of the market to fetch the order book for :param int [limit]: the maximum amount of order book entries to return :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order book structures ` indexed by market symbols """ self.load_markets() market = self.market(symbol) request: dict = { 'instId': market['id'], } limit = 50 if (limit is None) else limit if limit is not None: request['size'] = limit # max 100 response = self.publicGetMarketBooks(self.extend(request, params)) # # { # "code": "0", # "msg": "", # "data": [ # { # "asks": [ # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders # ["0.0723","299.880364","0","2"], # ["0.07231","3.72832","0","1"], # ], # "bids": [ # ["0.07221","18.5","0","1"], # ["0.0722","18.5","0","1"], # ["0.07219","0.505407","0","1"], # ], # "ts": "1621438475342" # } # ] # } # data = self.safe_list(response, 'data', []) first = self.safe_dict(data, 0, {}) timestamp = self.safe_integer(first, 'ts') return self.parse_order_book(first, symbol, timestamp) def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: # # response similar for REST & WS # # { # instId: "ADA-USDT", # ts: "1707736811486", # last: "0.5315", # lastSize: "4", # askPrice: "0.5318", # askSize: "248", # bidPrice: "0.5315", # bidSize: "63", # open24h: "0.5555", # high24h: "0.5563", # low24h: "0.5315", # volCurrency24h: "198560100", # vol24h: "1985601", # } # timestamp = self.safe_integer(ticker, 'ts') marketId = self.safe_string(ticker, 'instId') market = self.safe_market(marketId, market, '-') symbol = market['symbol'] last = self.safe_string(ticker, 'last') open = self.safe_string(ticker, 'open24h') spot = self.safe_bool(market, 'spot', False) quoteVolume = self.safe_string(ticker, 'volCurrency24h') if spot else None baseVolume = self.safe_string(ticker, 'vol24h') high = self.safe_string(ticker, 'high24h') low = self.safe_string(ticker, 'low24h') return self.safe_ticker({ 'symbol': symbol, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'high': high, 'low': low, 'bid': self.safe_string(ticker, 'bidPrice'), 'bidVolume': self.safe_string(ticker, 'bidSize'), 'ask': self.safe_string(ticker, 'askPrice'), 'askVolume': self.safe_string(ticker, 'askSize'), 'vwap': None, 'open': open, 'close': last, 'last': last, 'previousClose': None, 'change': None, 'percentage': None, 'average': None, 'baseVolume': baseVolume, 'quoteVolume': quoteVolume, 'indexPrice': self.safe_string(ticker, 'indexPrice'), 'markPrice': self.safe_string(ticker, 'markPrice'), 'info': ticker, }, 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://blofin.com/docs#get-tickers :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 ` """ self.load_markets() market = self.market(symbol) request: dict = { 'instId': market['id'], } response = self.publicGetMarketTickers(self.extend(request, params)) data = self.safe_list(response, 'data', []) first = self.safe_dict(data, 0, {}) return self.parse_ticker(first, market) def fetch_mark_price(self, symbol: str, params={}) -> Ticker: """ fetches mark price for the market https://docs.blofin.com/index.html#get-mark-price :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.subType]: "linear" or "inverse" :returns dict: a dictionary of `ticker structures ` """ self.load_markets() market = self.market(symbol) request = { 'symbol': market['id'], } response = self.publicGetMarketMarkPrice(self.extend(request, params)) data = self.safe_list(response, 'data', []) first = self.safe_dict(data, 0, {}) return self.parse_ticker(first, market) def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market https://blofin.com/docs#get-tickers :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a dictionary of `ticker structures ` """ self.load_markets() symbols = self.market_symbols(symbols) response = self.publicGetMarketTickers(params) tickers = self.safe_list(response, 'data', []) return self.parse_tickers(tickers, symbols) def parse_trade(self, trade: dict, market: Market = None) -> Trade: # # fetch trades(response similar for REST & WS) # # { # "tradeId": "3263934920", # "instId": "LTC-USDT", # "price": "67.87", # "size": "1", # "side": "buy", # "ts": "1707232020854" # } # # my trades # { # "instId": "LTC-USDT", # "tradeId": "1440847", # "orderId": "2075705202", # "fillPrice": "67.850000000000000000", # "fillSize": "1.000000000000000000", # "fillPnl": "0.000000000000000000", # "side": "buy", # "positionSide": "net", # "fee": "0.040710000000000000", # "ts": "1707224678878", # "brokerId": "" # } # id = self.safe_string(trade, 'tradeId') marketId = self.safe_string(trade, 'instId') market = self.safe_market(marketId, market, '-') symbol = market['symbol'] timestamp = self.safe_integer(trade, 'ts') price = self.safe_string_2(trade, 'price', 'fillPrice') amount = self.safe_string_2(trade, 'size', 'fillSize') side = self.safe_string(trade, 'side') orderId = self.safe_string(trade, 'orderId') feeCost = self.safe_string(trade, 'fee') fee = None if feeCost is not None: fee = { 'cost': feeCost, 'currency': market['settle'], } return self.safe_trade({ 'info': trade, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'id': id, 'order': orderId, 'type': None, 'takerOrMaker': None, 'side': side, 'price': price, 'amount': amount, 'cost': None, 'fee': fee, }, market) 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://blofin.com/docs#get-trades :param str symbol: unified symbol of the market to fetch trades for :param int [since]: timestamp in ms of the earliest trade to fetch :param int [limit]: the maximum amount of trades to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times :returns Trade[]: a list of `trade structures ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') if paginate: return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) market = self.market(symbol) request: dict = { 'instId': market['id'], } response = None if limit is not None: request['limit'] = limit # default 100 method = None method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') if method == 'publicGetMarketTrades': response = self.publicGetMarketTrades(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_trades(data, market, since, limit) def parse_ohlcv(self, ohlcv, market: Market = None) -> list: # # [ # "1678928760000", # timestamp # "24341.4", # open # "24344", # high # "24313.2", # low # "24323", # close # "628", # contract volume # "2.5819", # base volume # "62800", # quote volume # "0" # candlestick state # ] # return [ self.safe_integer(ohlcv, 0), self.safe_number(ohlcv, 1), self.safe_number(ohlcv, 2), self.safe_number(ohlcv, 3), self.safe_number(ohlcv, 4), self.safe_number(ohlcv, 6), ] def fetch_ohlcv(self, symbol: str, timeframe: str = '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://blofin.com/docs#get-candlesticks :param str symbol: unified symbol of the market to fetch OHLCV data for :param str timeframe: the length of time each candle represents :param int [since]: timestamp in ms of the earliest candle to fetch :param int [limit]: the maximum amount of candles to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: timestamp in ms of the latest candle 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 int[][]: A list of candles ordered, open, high, low, close, volume """ self.load_markets() market = self.market(symbol) 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, 100) if limit is None: limit = 100 # default 100, max 100 request: dict = { 'instId': market['id'], 'bar': self.safe_string(self.timeframes, timeframe, timeframe), 'limit': limit, } until = self.safe_integer(params, 'until') if until is not None: request['after'] = until params = self.omit(params, 'until') response = None response = self.publicGetMarketCandles(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_ohlcvs(data, market, timeframe, since, limit) def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): """ fetches historical funding rate prices https://blofin.com/docs#get-funding-rate-history :param str symbol: unified symbol of the market to fetch the funding rate history for :param int [since]: timestamp in ms of the earliest funding rate to fetch :param int [limit]: the maximum amount of `funding rate structures ` to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :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) :param int [params.until]: timestamp in ms of the latest funding rate to fetch :returns dict[]: a list of `funding rate structures ` """ if symbol is None: raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') if paginate: return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) market = self.market(symbol) request: dict = { 'instId': market['id'], } if since is not None: request['before'] = max(since - 1, 0) if limit is not None: request['limit'] = limit until = self.safe_integer(params, 'until') if until is not None: request['after'] = until params = self.omit(params, 'until') response = self.publicGetMarketFundingRateHistory(self.extend(request, params)) rates = [] data = self.safe_list(response, 'data', []) for i in range(0, len(data)): rate = data[i] timestamp = self.safe_integer(rate, 'fundingTime') rates.append({ 'info': rate, 'symbol': market['symbol'], 'fundingRate': self.safe_number(rate, 'fundingRate'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), }) sorted = self.sort_by(rates, 'timestamp') return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: # # { # "fundingRate": "0.00027815", # "fundingTime": "1634256000000", # "instId": "BTC-USD-SWAP", # } # marketId = self.safe_string(contract, 'instId') symbol = self.safe_symbol(marketId, market) fundingTime = self.safe_integer(contract, 'fundingTime') # > The current interest is 0. return { 'info': contract, 'symbol': symbol, 'markPrice': None, 'indexPrice': None, 'interestRate': self.parse_number('0'), 'estimatedSettlePrice': None, 'timestamp': None, 'datetime': None, 'fundingRate': self.safe_number(contract, 'fundingRate'), 'fundingTimestamp': fundingTime, 'fundingDatetime': self.iso8601(fundingTime), 'nextFundingRate': None, 'nextFundingTimestamp': None, 'nextFundingDatetime': None, 'previousFundingRate': None, 'previousFundingTimestamp': None, 'previousFundingDatetime': None, 'interval': None, } def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: """ fetch the current funding rate https://blofin.com/docs#get-funding-rate :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `funding rate structure ` """ self.load_markets() market = self.market(symbol) if not market['swap']: raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') request: dict = { 'instId': market['id'], } response = self.publicGetMarketFundingRate(self.extend(request, params)) # # { # "code": "0", # "data": [ # { # "fundingRate": "0.00027815", # "fundingTime": "1634256000000", # "instId": "BTC-USD-SWAP", # } # ], # "msg": "" # } # data = self.safe_list(response, 'data', []) entry = self.safe_dict(data, 0, {}) return self.parse_funding_rate(entry, market) def parse_balance_by_type(self, response): data = self.safe_list(response, 'data') if (data is not None) and isinstance(data, list): return self.parse_funding_balance(response) else: return self.parse_balance(response) def parse_balance(self, response): # # "data" similar for REST & WS # # { # "code": "0", # "msg": "success", # "data": { # "ts": "1697021343571", # "totalEquity": "10011254.077985990315787910", # "isolatedEquity": "861.763132108800000000", # "details": [ # { # "currency": "USDT", # "equity": "10014042.988958415234430699548", # "balance": "10013119.885958415234430699", # "ts": "1697021343571", # "isolatedEquity": "862.003200000000000000048", # "available": "9996399.4708691159703362725", # "availableEquity": "9996399.4708691159703362725", # "frozen": "15805.149672632597427761", # "orderFrozen": "14920.994472632597427761", # "equityUsd": "10011254.077985990315787910", # "isolatedUnrealizedPnl": "-22.151999999999999999952", # "bonus": "0" # present only in REST # "unrealizedPnl": "0" # present only in WS # } # ] # } # } # result: dict = {'info': response} data = self.safe_dict(response, 'data', {}) timestamp = self.safe_integer(data, 'ts') details = self.safe_list(data, 'details', []) for i in range(0, len(details)): balance = details[i] currencyId = self.safe_string(balance, 'currency') code = self.safe_currency_code(currencyId) account = self.account() # it may be incorrect to use total, free and used for swap accounts eq = self.safe_string(balance, 'equity') availEq = self.safe_string(balance, 'available') if (eq is None) or (availEq is None): account['free'] = self.safe_string(balance, 'availableEquity') account['used'] = self.safe_string(balance, 'frozen') else: account['total'] = eq account['free'] = availEq result[code] = account result['timestamp'] = timestamp result['datetime'] = self.iso8601(timestamp) return self.safe_balance(result) def parse_funding_balance(self, response): # # { # "code": "0", # "msg": "success", # "data": [ # { # "currency": "USDT", # "balance": "10012514.919418081548717298", # "available": "9872132.414278782284622898", # "frozen": "138556.471805965930761067", # "bonus": "0" # } # ] # } # result: dict = {'info': response} data = self.safe_list(response, 'data', []) for i in range(0, len(data)): balance = data[i] currencyId = self.safe_string(balance, 'currency') code = self.safe_currency_code(currencyId) account = self.account() # it may be incorrect to use total, free and used for swap accounts account['total'] = self.safe_string(balance, 'balance') account['free'] = self.safe_string(balance, 'available') account['used'] = self.safe_string(balance, 'frozen') result[code] = account return self.safe_balance(result) def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: return { 'info': fee, 'symbol': self.safe_symbol(None, market), # blofin returns the fees values opposed to other exchanges, so the sign needs to be flipped 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), 'percentage': None, 'tierBased': 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://blofin.com/docs#get-balance https://blofin.com/docs#get-futures-account-balance :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.accountType]: the type of account to fetch the balance for, either 'funding' or 'futures' or 'copy_trading' or 'earn' :returns dict: a `balance structure ` """ self.load_markets() accountType = None accountType, params = self.handle_option_and_params_2(params, 'fetchBalance', 'accountType', 'type') request: dict = { } response = None if accountType is not None and accountType != 'swap': options = self.safe_dict(self.options, 'accountsByType', {}) parsedAccountType = self.safe_string(options, accountType, accountType) request['accountType'] = parsedAccountType response = self.privateGetAssetBalances(self.extend(request, params)) else: response = self.privateGetAccountBalance(self.extend(request, params)) return self.parse_balance_by_type(response) def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): market = self.market(symbol) request: dict = { 'instId': market['id'], 'side': side, 'orderType': type, 'size': self.amount_to_precision(symbol, amount), 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), } marginMode = None marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross') request['marginMode'] = marginMode triggerPrice = self.safe_string(params, 'triggerPrice') timeInForce = self.safe_string(params, 'timeInForce', 'GTC') isHedged = self.safe_bool(params, 'hedged', False) if isHedged: request['positionSide'] = 'long' if (side == 'buy') else 'short' isMarketOrder = type == 'market' params = self.omit(params, ['timeInForce']) ioc = (timeInForce == 'IOC') or (type == 'ioc') marketIOC = (isMarketOrder and ioc) if isMarketOrder or marketIOC: request['orderType'] = 'market' else: key = 'orderPrice' if (triggerPrice is not None) else 'price' request[key] = self.price_to_precision(symbol, price) postOnly = False postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) if postOnly: request['type'] = 'post_only' stopLoss = self.safe_dict(params, 'stopLoss') takeProfit = self.safe_dict(params, 'takeProfit') params = self.omit(params, ['stopLoss', 'takeProfit', 'hedged']) isStopLoss = stopLoss is not None isTakeProfit = takeProfit is not None if isStopLoss or isTakeProfit: if isStopLoss: slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice') request['slTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice) slOrderPrice = self.safe_string(stopLoss, 'price', '-1') request['slOrderPrice'] = self.price_to_precision(symbol, slOrderPrice) if isTakeProfit: tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice') request['tpTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice) tpPrice = self.safe_string(takeProfit, 'price', '-1') request['tpOrderPrice'] = self.price_to_precision(symbol, tpPrice) elif triggerPrice is not None: request['orderType'] = 'trigger' request['triggerPrice'] = self.price_to_precision(symbol, triggerPrice) if isMarketOrder: request['orderPrice'] = '-1' return self.extend(request, params) def parse_order_status(self, status: Str): statuses: dict = { 'canceled': 'canceled', 'order_failed': 'canceled', 'live': 'open', 'partially_filled': 'open', 'filled': 'closed', 'effective': 'closed', } return self.safe_string(statuses, status, status) def parse_order(self, order: dict, market: Market = None) -> Order: # # response similar for REST & WS # # { # "orderId": "2075628533", # "clientOrderId": "", # "instId": "LTC-USDT", # "marginMode": "cross", # "positionSide": "net", # "side": "buy", # "orderType": "market", # "price": "0.000000000000000000", # "size": "1.000000000000000000", # "reduceOnly": "true", # "leverage": "3", # "state": "filled", # "filledSize": "1.000000000000000000", # "pnl": "-0.050000000000000000", # "averagePrice": "68.110000000000000000", # "fee": "0.040866000000000000", # "createTime": "1706891359010", # "updateTime": "1706891359098", # "orderCategory": "normal", # "tpTriggerPrice": null, # "tpOrderPrice": null, # "slTriggerPrice": null, # "slOrderPrice": null, # "cancelSource": "not_canceled", # "cancelSourceReason": null, # "brokerId": "ec6dd3a7dd982d0b" # "filled_amount": "1.000000000000000000", # filledAmount in "ws" watchOrders # "cancelSource": "", # only in WS # "instType": "SWAP", # only in WS # } # id = self.safe_string_n(order, ['tpslId', 'orderId', 'algoId']) timestamp = self.safe_integer(order, 'createTime') lastUpdateTimestamp = self.safe_integer(order, 'updateTime') lastTradeTimestamp = self.safe_integer(order, 'fillTime') side = self.safe_string(order, 'side') type = self.safe_string(order, 'orderType') postOnly = None timeInForce = None if type == 'post_only': postOnly = True type = 'limit' elif type == 'fok': timeInForce = 'FOK' type = 'limit' elif type == 'ioc': timeInForce = 'IOC' type = 'limit' elif type == 'conditional': type = 'trigger' marketId = self.safe_string(order, 'instId') market = self.safe_market(marketId, market) symbol = self.safe_symbol(marketId, market, '-') filled = self.safe_string(order, 'filledSize') price = self.safe_string_n(order, ['px', 'price', 'orderPrice']) average = self.safe_string(order, 'averagePrice') status = self.parse_order_status(self.safe_string(order, 'state')) feeCostString = self.safe_string(order, 'fee') amount = self.safe_string(order, 'size') leverage = self.safe_string(order, 'leverage', '1') contractSize = self.safe_string(market, 'contractSize') baseAmount = Precise.string_mul(contractSize, filled) cost: Str = None if average is not None: cost = Precise.string_mul(average, baseAmount) cost = Precise.string_div(cost, leverage) # spot market buy: "sz" can refer either to base currency units or to quote currency units fee = None if feeCostString is not None: feeCostSigned = Precise.string_abs(feeCostString) feeCurrencyId = self.safe_string(order, 'feeCcy', 'USDT') feeCurrencyCode = self.safe_currency_code(feeCurrencyId) fee = { 'cost': self.parse_number(feeCostSigned), 'currency': feeCurrencyCode, } clientOrderId = self.safe_string(order, 'clientOrderId') if (clientOrderId is not None) and (len(clientOrderId) < 1): clientOrderId = None # fix empty clientOrderId string stopLossTriggerPrice = self.safe_number(order, 'slTriggerPrice') stopLossPrice = self.safe_number(order, 'slOrderPrice') takeProfitTriggerPrice = self.safe_number(order, 'tpTriggerPrice') takeProfitPrice = self.safe_number(order, 'tpOrderPrice') reduceOnlyRaw = self.safe_string(order, 'reduceOnly') reduceOnly = (reduceOnlyRaw == 'true') return self.safe_order({ 'info': order, 'id': id, 'clientOrderId': clientOrderId, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastTradeTimestamp': lastTradeTimestamp, 'lastUpdateTimestamp': lastUpdateTimestamp, 'symbol': symbol, 'type': type, 'timeInForce': timeInForce, 'postOnly': postOnly, 'side': side, 'price': price, 'stopLossTriggerPrice': stopLossTriggerPrice, 'takeProfitTriggerPrice': takeProfitTriggerPrice, 'stopLossPrice': stopLossPrice, 'takeProfitPrice': takeProfitPrice, 'average': average, 'cost': cost, 'amount': amount, 'filled': filled, 'remaining': None, 'status': status, 'fee': fee, 'trades': None, 'reduceOnly': reduceOnly, }, market) def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order: """ create a trade order https://blofin.com/docs#place-order https://blofin.com/docs#place-tpsl-order :param str symbol: unified symbol of the market to create an order in :param str type: 'market' or 'limit' or 'post_only' or 'ioc' or 'fok' :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.triggerPrice]: the trigger price for a trigger order :param bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders :param bool [params.postOnly]: True to place a post only order :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross' :param float [params.stopLossPrice]: stop loss trigger price(will use privatePostTradeOrderTpsl) :param float [params.takeProfitPrice]: take profit trigger price(will use privatePostTradeOrderTpsl) :param str [params.positionSide]: *stopLossPrice/takeProfitPrice orders only* 'long' or 'short' or 'net' default is 'net' :param boolean [params.hedged]: if True, the positionSide will be set to long/short instead of net, default is False :param str [params.clientOrderId]: a unique id for the order :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered :param float [params.takeProfit.triggerPrice]: take profit trigger price :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order) :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered :param float [params.stopLoss.triggerPrice]: stop loss trigger price :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order) :returns dict: an `order structure ` """ self.load_markets() market = self.market(symbol) tpsl = self.safe_bool(params, 'tpsl', False) params = self.omit(params, 'tpsl') method = None method, params = self.handle_option_and_params(params, 'createOrder', 'method', 'privatePostTradeOrder') isStopLossPriceDefined = self.safe_string(params, 'stopLossPrice') is not None isTakeProfitPriceDefined = self.safe_string(params, 'takeProfitPrice') is not None hasTriggerPrice = self.safe_string(params, 'triggerPrice') is not None isType2Order = (isStopLossPriceDefined or isTakeProfitPriceDefined) response = None reduceOnly = self.safe_bool(params, 'reduceOnly') if reduceOnly is not None: params['reduceOnly'] = 'true' if reduceOnly else 'false' isTpslOrder = tpsl or (method == 'privatePostTradeOrderTpsl') or isType2Order isTriggerOrder = hasTriggerPrice or (method == 'privatePostTradeOrderAlgo') if isTpslOrder: tpslRequest = self.create_tpsl_order_request(symbol, type, side, amount, price, params) response = self.privatePostTradeOrderTpsl(tpslRequest) elif isTriggerOrder: triggerRequest = self.create_order_request(symbol, type, side, amount, price, params) response = self.privatePostTradeOrderAlgo(triggerRequest) else: request = self.create_order_request(symbol, type, side, amount, price, params) response = self.privatePostTradeOrder(request) if isTpslOrder or isTriggerOrder: dataDict = self.safe_dict(response, 'data', {}) return self.parse_order(dataDict, market) data = self.safe_list(response, 'data', []) first = self.safe_dict(data, 0) order = self.parse_order(first, market) order['type'] = type order['side'] = side return order def create_tpsl_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): market = self.market(symbol) positionSide = self.safe_string(params, 'positionSide', 'net') request: dict = { 'instId': market['id'], 'side': side, 'positionSide': positionSide, 'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'), } if amount is not None: request['size'] = self.amount_to_precision(symbol, amount) marginMode = self.safe_string(params, 'marginMode', 'cross') # cross or isolated if marginMode != 'cross' and marginMode != 'isolated': raise BadRequest(self.id + ' createTpslOrder() requires a marginMode parameter that must be either cross or isolated') stopLossPrice = self.safe_string(params, 'stopLossPrice') takeProfitPrice = self.safe_string(params, 'takeProfitPrice') if stopLossPrice is not None: request['slTriggerPrice'] = self.price_to_precision(symbol, stopLossPrice) if type == 'market': request['slOrderPrice'] = '-1' else: request['slOrderPrice'] = self.price_to_precision(symbol, price) elif takeProfitPrice is not None: request['tpTriggerPrice'] = self.price_to_precision(symbol, takeProfitPrice) if type == 'market': request['tpOrderPrice'] = '-1' else: request['tpOrderPrice'] = self.price_to_precision(symbol, price) request['marginMode'] = marginMode params = self.omit(params, ['stopLossPrice', 'takeProfitPrice']) return self.extend(request, params) def cancel_order(self, id: str, symbol: Str = None, params={}): """ cancels an open order https://blofin.com/docs#cancel-order https://blofin.com/docs#cancel-tpsl-order :param str id: order id :param str symbol: unified symbol of the market the order was made in :param dict [params]: extra parameters specific to the exchange API endpoint :param boolean [params.trigger]: True if cancelling a trigger/conditional :param boolean [params.tpsl]: True if cancelling a tpsl order :returns dict: An `order structure ` """ if symbol is None: raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') self.load_markets() market = self.market(symbol) request: dict = { 'instId': market['id'], } isTrigger = self.safe_bool_n(params, ['trigger'], False) isTpsl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) clientOrderId = self.safe_string(params, 'clientOrderId') if clientOrderId is not None: request['clientOrderId'] = clientOrderId else: if not isTrigger and not isTpsl: request['orderId'] = str(id) elif isTpsl: request['tpslId'] = str(id) elif isTrigger: request['algoId'] = str(id) query = self.omit(params, ['orderId', 'clientOrderId', 'stop', 'trigger', 'tpsl']) if isTpsl: tpslResponse = self.cancel_orders([id], symbol, params) first = self.safe_dict(tpslResponse, 0) return first elif isTrigger: triggerResponse = self.privatePostTradeCancelAlgo(self.extend(request, query)) triggerData = self.safe_dict(triggerResponse, 'data') return self.parse_order(triggerData, market) response = self.privatePostTradeCancelOrder(self.extend(request, query)) data = self.safe_list(response, 'data', []) order = self.safe_dict(data, 0) return self.parse_order(order, market) def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]: """ create a list of trade orders https://blofin.com/docs#place-multiple-orders :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an `order structure ` """ self.load_markets() ordersRequests = [] for i in range(0, len(orders)): rawOrder = orders[i] marketId = self.safe_string(rawOrder, 'symbol') type = self.safe_string(rawOrder, 'type') side = self.safe_string(rawOrder, 'side') amount = self.safe_value(rawOrder, 'amount') price = self.safe_value(rawOrder, 'price') orderParams = self.safe_dict(rawOrder, 'params', {}) extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) ordersRequests.append(orderRequest) response = self.privatePostTradeBatchOrders(ordersRequests) data = self.safe_list(response, 'data', []) return self.parse_orders(data) def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ Fetch orders that are still open https://blofin.com/docs#get-active-orders https://blofin.com/docs#get-active-tpsl-orders https://docs.blofin.com/index.html#get-active-algo-orders :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch open orders for :param int [limit]: the maximum number of open orders structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param bool [params.trigger]: True if fetching trigger or conditional orders :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 Order[]: a list of `order structures ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) request: dict = { } market = None if symbol is not None: market = self.market(symbol) request['instId'] = market['id'] if limit is not None: request['limit'] = limit # default 100, max 100 isTrigger = self.safe_bool_n(params, ['stop', 'trigger'], False) isTpSl = self.safe_bool_2(params, 'tpsl', 'TPSL', False) method: Str = None method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersPending') query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) response = None if isTpSl or (method == 'privateGetTradeOrdersTpslPending'): response = self.privateGetTradeOrdersTpslPending(self.extend(request, query)) elif isTrigger or (method == 'privateGetTradeOrdersAlgoPending'): request['orderType'] = 'trigger' response = self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) else: response = self.privateGetTradeOrdersPending(self.extend(request, query)) data = self.safe_list(response, 'data', []) return self.parse_orders(data, 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://blofin.com/docs#get-trade-history :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]: Timestamp in ms of the latest time to retrieve trades for :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 ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) request: dict = { } market = None if symbol is not None: market = self.market(symbol) request['instId'] = market['id'] request, params = self.handle_until_option('end', request, params) if limit is not None: request['limit'] = limit # default 100, max 100 response = self.privateGetTradeFillsHistory(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_trades(data, market, since, limit) def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: """ fetch all deposits made to an account https://blofin.com/docs#get-deposite-history :param str code: unified currency code :param int [since]: the earliest time in ms to fetch deposits for :param int [limit]: the maximum number of deposits 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 entries for :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 dict[]: a list of `transaction structures ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) request: dict = { } currency = None if code is not None: currency = self.currency(code) request['currency'] = currency['id'] if since is not None: request['before'] = max(since - 1, 0) if limit is not None: request['limit'] = limit # default 100, max 100 request, params = self.handle_until_option('after', request, params) response = self.privateGetAssetDepositHistory(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_transactions(data, currency, since, limit, params) def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: """ fetch all withdrawals made from an account https://blofin.com/docs#get-withdraw-history :param str code: unified currency code :param int [since]: the earliest time in ms to fetch withdrawals for :param int [limit]: the maximum number of withdrawals 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 entries for :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 dict[]: a list of `transaction structures ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) request: dict = { } currency = None if code is not None: currency = self.currency(code) request['currency'] = currency['id'] if since is not None: request['before'] = max(since - 1, 0) if limit is not None: request['limit'] = limit # default 100, max 100 request, params = self.handle_until_option('after', request, params) response = self.privateGetAssetWithdrawalHistory(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_transactions(data, currency, since, limit, params) def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: """ fetch the history of changes, actions done by the user or operations that altered the balance of the user https://blofin.com/docs#get-funds-transfer-history :param str [code]: unified currency code, default is None :param int [since]: timestamp in ms of the earliest ledger entry, default is None :param int [limit]: max number of ledger entries to return, default is None :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.marginMode]: 'cross' or 'isolated' :param int [params.until]: the latest time in ms to fetch entries for :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) :returns dict: a `ledger structure ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) request: dict = { } if limit is not None: request['limit'] = limit currency = None if code is not None: currency = self.currency(code) request['currency'] = currency['id'] request, params = self.handle_until_option('end', request, params) response = None response = self.privateGetAssetBills(self.extend(request, params)) data = self.safe_list(response, 'data', []) return self.parse_ledger(data, currency, since, limit) def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: # # # fetchDeposits # # { # "currency": "USDT", # "chain": "TRC20", # "address": "TGfJLtnsh3B9EqekFEBZ1nR14QanBUf5Bi", # "txId": "892f4e0c32268b29b2e541ef30d32a30bbf10f902adcc4b1428319ed7c3758fd", # "type": "0", # "amount": "86.975843", # "state": "1", # "ts": "1703163304153", # "tag": null, # "confirm": "16", # "depositId": "36c8e2a7ea184a219de72215a696acaf" # } # fetchWithdrawals # { # "currency": "USDT", # "chain": "TRC20", # "address": "TYgB3sVXHPEDQUu288EG1uMFh9Pk2swLgW", # "txId": "1fd5ac52df414d7ea66194cadd9a5b4d2422c2b9720037f66d98207f9858fd96", # "type": "0", # "amount": "9", # "fee": "1", # "feeCurrency": "USDT", # "state": "3", # "clientId": null, # "ts": "1707217439351", # "tag": null, # "memo": null, # "withdrawId": "e0768698cfdf4aee8e54654c3775914b" # } # type = None id = None withdrawalId = self.safe_string(transaction, 'withdrawId') depositId = self.safe_string(transaction, 'depositId') addressTo = self.safe_string(transaction, 'address') address = addressTo tagTo = self.safe_string(transaction, 'tag') if withdrawalId is not None: type = 'withdrawal' id = withdrawalId else: id = depositId type = 'deposit' currencyId = self.safe_string(transaction, 'currency') code = self.safe_currency_code(currencyId) amount = self.safe_number(transaction, 'amount') status = self.parse_transaction_status(self.safe_string(transaction, 'state')) txid = self.safe_string(transaction, 'txId') timestamp = self.safe_integer(transaction, 'ts') feeCurrencyId = self.safe_string(transaction, 'feeCurrency') feeCode = self.safe_currency_code(feeCurrencyId) feeCost = self.safe_number(transaction, 'fee') return { 'info': transaction, 'id': id, 'currency': code, 'amount': amount, 'network': None, 'addressFrom': None, 'addressTo': addressTo, 'address': address, 'tagFrom': None, 'tagTo': tagTo, 'tag': tagTo, 'status': status, 'type': type, 'updated': None, 'txid': txid, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'internal': None, 'comment': None, 'fee': { 'currency': feeCode, 'cost': feeCost, }, } def parse_transaction_status(self, status: Str): statuses: dict = { '0': 'pending', '1': 'ok', '2': 'failed', '3': 'pending', } return self.safe_string(statuses, status, status) def parse_ledger_entry_type(self, type): types: dict = { '1': 'transfer', # transfer '2': 'trade', # trade '3': 'trade', # delivery '4': 'rebate', # auto token conversion '5': 'trade', # liquidation '6': 'transfer', # margin transfer '7': 'trade', # interest deduction '8': 'fee', # funding rate '9': 'trade', # adl '10': 'trade', # clawback '11': 'trade', # system token conversion } return self.safe_string(types, type, type) def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: currencyId = self.safe_string(item, 'currency') code = self.safe_currency_code(currencyId, currency) currency = self.safe_currency(currencyId, currency) timestamp = self.safe_integer(item, 'ts') return self.safe_ledger_entry({ 'info': item, 'id': self.safe_string(item, 'transferId'), 'direction': None, 'account': None, 'referenceId': self.safe_string(item, 'clientId'), 'referenceAccount': None, 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), 'currency': code, 'amount': self.safe_number(item, 'amount'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'before': None, 'after': None, 'status': 'ok', 'fee': None, }, currency) def parse_ids(self, ids): """ @ignore :param string[]|str ids: order ids :returns str[]: list of order ids """ if isinstance(ids, str): return ids.split(',') else: return ids def cancel_orders(self, ids: List[str], symbol: Str = None, params={}): """ cancel multiple orders https://blofin.com/docs#cancel-multiple-orders :param str[] ids: order ids :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param boolean [params.trigger]: whether the order is a stop/trigger order :returns dict: an list of `order structures ` """ # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. if symbol is None: raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') self.load_markets() market = self.market(symbol) request = [] options = self.safe_dict(self.options, 'cancelOrders', {}) defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') method = self.safe_string(params, 'method', defaultMethod) clientOrderIds = self.parse_ids(self.safe_value(params, 'clientOrderId')) tpslIds = self.parse_ids(self.safe_value(params, 'tpslId')) trigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl']) if trigger: method = 'privatePostTradeCancelTpsl' if clientOrderIds is None: ids = self.parse_ids(ids) if tpslIds is not None: for i in range(0, len(tpslIds)): request.append({ 'tpslId': tpslIds[i], 'instId': market['id'], }) for i in range(0, len(ids)): if trigger: request.append({ 'tpslId': ids[i], 'instId': market['id'], }) else: request.append({ 'orderId': ids[i], 'instId': market['id'], }) else: for i in range(0, len(clientOrderIds)): request.append({ 'instId': market['id'], 'clientOrderId': clientOrderIds[i], }) response = None if method == 'privatePostTradeCancelTpsl': response = self.privatePostTradeCancelTpsl(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT else: response = self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT ordersData = self.safe_list(response, 'data', []) return self.parse_orders(ordersData, market, None, None, params) def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: """ transfer currency internally between wallets on the same account https://blofin.com/docs#funds-transfer :param str code: unified currency code :param float amount: amount to transfer :param str fromAccount: account to transfer from(funding, swap, copy_trading, earn) :param str toAccount: account to transfer to(funding, swap, copy_trading, earn) :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `transfer structure ` """ self.load_markets() currency = self.currency(code) accountsByType = self.safe_dict(self.options, 'accountsByType', {}) fromId = self.safe_string(accountsByType, fromAccount, fromAccount) toId = self.safe_string(accountsByType, toAccount, toAccount) request: dict = { 'currency': currency['id'], 'amount': self.currency_to_precision(code, amount), 'fromAccount': fromId, 'toAccount': toId, } response = self.privatePostAssetTransfer(self.extend(request, params)) data = self.safe_dict(response, 'data', {}) return self.parse_transfer(data, currency) def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: id = self.safe_string(transfer, 'transferId') return { 'info': transfer, 'id': id, 'timestamp': None, 'datetime': None, 'currency': None, 'amount': None, 'fromAccount': None, 'toAccount': None, 'status': None, } def fetch_position(self, symbol: str, params={}) -> Position: """ fetch data on a single open contract trade position https://blofin.com/docs#get-positions :param str symbol: unified market symbol of the market the position is held in, default is None :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION :returns dict: a `position structure ` """ self.load_markets() market = self.market(symbol) request: dict = { 'instId': market['id'], } response = self.privateGetAccountPositions(self.extend(request, params)) data = self.safe_list(response, 'data', []) position = self.safe_dict(data, 0) if position is None: return None return self.parse_position(position, market) def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]: """ fetch data on a single open contract trade position https://blofin.com/docs#get-positions :param str[] [symbols]: list of unified market symbols :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION :returns dict: a `position structure ` """ self.load_markets() symbols = self.market_symbols(symbols) response = self.privateGetAccountPositions(params) data = self.safe_list(response, 'data', []) result = self.parse_positions(data) return self.filter_by_array_positions(result, 'symbol', symbols, False) def parse_position(self, position: dict, market: Market = None): # # response similar for REST & WS # # { # instType: 'SWAP', # instId: 'LTC-USDT', # marginMode: 'cross', # positionId: '644159', # positionSide: 'net', # positions: '1', # availablePositions: '1', # averagePrice: '68.16', # unrealizedPnl: '0.80631223', # unrealizedPnlRatio: '0.03548909463028169', # leverage: '3', # liquidationPrice: '10.116655172370356435', # markPrice: '68.96', # initialMargin: '22.988770743333333333', # margin: '', # self field might not exist in rest response # marginRatio: '152.523509620342499273', # maintenanceMargin: '0.34483156115', # adl: '4', # createTime: '1707235776528', # updateTime: '1707235776528' # } # marketId = self.safe_string(position, 'instId') market = self.safe_market(marketId, market) symbol = market['symbol'] pos = self.safe_string(position, 'positions') contractsAbs = Precise.string_abs(pos) side = self.safe_string(position, 'positionSide') hedged = side != 'net' contracts = self.parse_number(contractsAbs) if pos is not None: if side == 'net': if Precise.string_gt(pos, '0'): side = 'long' elif Precise.string_lt(pos, '0'): side = 'short' else: side = None contractSize = self.safe_number(market, 'contractSize') contractSizeString = self.number_to_string(contractSize) markPriceString = self.safe_string(position, 'markPrice') notionalString = self.safe_string(position, 'notionalUsd') if market['inverse']: notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) notional = self.parse_number(notionalString) marginMode = self.safe_string(position, 'marginMode') initialMarginString = None entryPriceString = self.safe_string(position, 'averagePrice') unrealizedPnlString = self.safe_string(position, 'unrealizedPnl') leverageString = self.safe_string(position, 'leverage') initialMarginPercentage = None collateralString = None if marginMode == 'cross': initialMarginString = self.safe_string(position, 'initialMargin') collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) elif marginMode == 'isolated': initialMarginPercentage = Precise.string_div('1', leverageString) collateralString = self.safe_string(position, 'margin') maintenanceMarginString = self.safe_string(position, 'maintenanceMargin') maintenanceMargin = self.parse_number(maintenanceMarginString) maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) if initialMarginPercentage is None: initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) elif initialMarginString is None: initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) rounder = '0.00005' # round to closest 0.01% maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) liquidationPrice = self.safe_number(position, 'liquidationPrice') percentageString = self.safe_string(position, 'unrealizedPnlRatio') percentage = self.parse_number(Precise.string_mul(percentageString, '100')) timestamp = self.safe_integer(position, 'updateTime') marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) return self.safe_position({ 'info': position, 'id': None, 'symbol': symbol, 'notional': notional, 'marginMode': marginMode, 'liquidationPrice': liquidationPrice, 'entryPrice': self.parse_number(entryPriceString), 'unrealizedPnl': self.parse_number(unrealizedPnlString), 'percentage': percentage, 'contracts': contracts, 'contractSize': contractSize, 'markPrice': self.parse_number(markPriceString), 'lastPrice': None, 'side': side, 'hedged': hedged, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastUpdateTimestamp': None, 'maintenanceMargin': maintenanceMargin, 'maintenanceMarginPercentage': maintenanceMarginPercentage, 'collateral': self.parse_number(collateralString), 'initialMargin': self.parse_number(initialMarginString), 'initialMarginPercentage': self.parse_number(initialMarginPercentage), 'leverage': self.parse_number(leverageString), 'marginRatio': marginRatio, 'stopLossPrice': None, 'takeProfitPrice': None, }) def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages: """ fetch the set leverage for all contract markets https://docs.blofin.com/index.html#get-multiple-leverage :param str[] symbols: a list of unified market symbols, required on blofin :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.marginMode]: 'cross' or 'isolated' :returns dict: a list of `leverage structures ` """ self.load_markets() if symbols is None: raise ArgumentsRequired(self.id + ' fetchLeverages() requires a symbols argument') marginMode = None marginMode, params = self.handle_margin_mode_and_params('fetchLeverages', params) if marginMode is None: marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode if (marginMode != 'cross') and (marginMode != 'isolated'): raise BadRequest(self.id + ' fetchLeverages() requires a marginMode parameter that must be either cross or isolated') symbols = self.market_symbols(symbols) instIds = '' for i in range(0, len(symbols)): entry = symbols[i] entryMarket = self.market(entry) if i > 0: instIds = instIds + ',' + entryMarket['id'] else: instIds = instIds + entryMarket['id'] request: dict = { 'instId': instIds, 'marginMode': marginMode, } response = self.privateGetAccountBatchLeverageInfo(self.extend(request, params)) # # { # "code": "0", # "msg": "success", # "data": [ # { # "leverage": "3", # "marginMode": "cross", # "instId": "BTC-USDT" # }, # ] # } # leverages = self.safe_list(response, 'data', []) return self.parse_leverages(leverages, symbols, 'instId') def fetch_leverage(self, symbol: str, params={}) -> Leverage: """ fetch the set leverage for a market https://docs.blofin.com/index.html#get-leverage :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.marginMode]: 'cross' or 'isolated' :returns dict: a `leverage structure ` """ self.load_markets() marginMode = None marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) if marginMode is None: marginMode = self.safe_string(params, 'marginMode', 'cross') # cross marginMode if (marginMode != 'cross') and (marginMode != 'isolated'): raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') market = self.market(symbol) request: dict = { 'instId': market['id'], 'marginMode': marginMode, } response = self.privateGetAccountLeverageInfo(self.extend(request, params)) # # { # "code": "0", # "msg": "success", # "data": { # "leverage": "3", # "marginMode": "cross", # "instId": "BTC-USDT" # } # } # data = self.safe_dict(response, 'data', {}) return self.parse_leverage(data, market) def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: marketId = self.safe_string(leverage, 'instId') leverageValue = self.safe_integer(leverage, 'leverage') return { 'info': leverage, 'symbol': self.safe_symbol(marketId, market), 'marginMode': self.safe_string_lower(leverage, 'marginMode'), 'longLeverage': leverageValue, 'shortLeverage': leverageValue, } def set_leverage(self, leverage: int, symbol: Str = None, params={}): """ set the level of leverage for a market https://blofin.com/docs#set-leverage :param int leverage: the rate of leverage :param str symbol: unified market symbol :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.marginMode]: 'cross' or 'isolated' :param str [params.positionSide]: 'long' or 'short' - required for hedged mode in isolated margin :returns dict: response from the exchange """ if symbol is None: raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS if (leverage < 1) or (leverage > 125): raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') self.load_markets() market = self.market(symbol) marginMode = None marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross') if (marginMode != 'cross') and (marginMode != 'isolated'): raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') request: dict = { 'leverage': leverage, 'marginMode': marginMode, 'instId': market['id'], } response = self.privatePostAccountSetLeverage(self.extend(request, params)) return response def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: """ closes open positions for a market https://blofin.com/docs#close-positions :param str symbol: Unified CCXT market symbol :param str [side]: 'buy' or 'sell', leave in net mode :param dict [params]: extra parameters specific to the blofin api endpoint :param str [params.clientOrderId]: a unique identifier for the order :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency EXCHANGE SPECIFIC PARAMETERS :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters :returns dict[]: `A list of position structures ` """ self.load_markets() market = self.market(symbol) clientOrderId = self.safe_string(params, 'clientOrderId') marginMode = None marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') request: dict = { 'instId': market['id'], 'marginMode': marginMode, } if clientOrderId is not None: request['clientOrderId'] = clientOrderId response = self.privatePostTradeClosePosition(self.extend(request, params)) return self.safe_dict(response, 'data') 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://blofin.com/docs#get-order-history https://blofin.com/docs#get-tpsl-order-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 orde structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param bool [params.trigger]: True if fetching trigger or conditional orders :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 Order[]: a list of `order structures ` """ self.load_markets() paginate = False paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') if paginate: return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) request: dict = { } market = None if symbol is not None: market = self.market(symbol) request['instId'] = market['id'] if limit is not None: request['limit'] = limit # default 100, max 100 if since is not None: request['begin'] = since isTrigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl', 'TPSL'], False) method: Str = None method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersHistory') query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL']) response = None if (isTrigger) or (method == 'privateGetTradeOrdersTpslHistory'): response = self.privateGetTradeOrdersTpslHistory(self.extend(request, query)) else: response = self.privateGetTradeOrdersHistory(self.extend(request, query)) data = self.safe_list(response, 'data', []) return self.parse_orders(data, market, since, limit) def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode: """ fetches the margin mode of a trading pair https://docs.blofin.com/index.html#get-margin-mode :param str symbol: unified symbol of the market to fetch the margin mode for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `margin mode structure ` """ self.load_markets() market = self.market(symbol) response = self.privateGetAccountMarginMode(params) # # { # "code": "0", # "msg": "success", # "data": { # "marginMode": "cross" # } # } # data = self.safe_dict(response, 'data', {}) return self.parse_margin_mode(data, market) def parse_margin_mode(self, marginMode: dict, market: Market = None) -> MarginMode: return { 'info': marginMode, 'symbol': self.safe_string(market, 'symbol'), 'marginMode': self.safe_string(marginMode, 'marginMode'), } def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): """ set margin mode to 'cross' or 'isolated' https://docs.blofin.com/index.html#set-margin-mode :param str marginMode: 'cross' or 'isolated' :param str [symbol]: unified market symbol(not used in blofin setMarginMode) :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: response from the exchange """ self.check_required_argument('setMarginMode', marginMode, 'marginMode', ['cross', 'isolated']) self.load_markets() market = None if symbol is not None: market = self.market(symbol) request: dict = { 'marginMode': marginMode, } response = self.privatePostAccountSetMarginMode(self.extend(request, params)) # # { # "code": "0", # "msg": "success", # "data": { # "marginMode": "isolated" # } # } # data = self.safe_dict(response, 'data', {}) return self.parse_margin_mode(data, market) def fetch_position_mode(self, symbol: Str = None, params={}): """ fetchs the position mode, hedged or one way https://docs.blofin.com/index.html#get-position-mode :param str [symbol]: unified symbol of the market to fetch the position mode for(not used in blofin fetchPositionMode) :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an object detailing whether the market is in hedged or one-way mode """ response = self.privateGetAccountPositionMode(params) data = self.safe_dict(response, 'data', {}) positionMode = self.safe_string(data, 'positionMode') # # { # "code": "0", # "msg": "success", # "data": { # "positionMode": "long_short_mode" # } # } # return { 'info': data, 'hedged': positionMode == 'long_short_mode', } def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): """ set hedged to True or False for a market https://docs.blofin.com/index.html#set-position-mode :param bool hedged: set to True to use hedged mode, False for one-way mode :param str [symbol]: not used by blofin setPositionMode() :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: response from the exchange """ request: dict = { 'positionMode': 'long_short_mode' if hedged else 'net_mode', } # # { # "code": "0", # "msg": "success", # "data": { # "positionMode": "net_mode" # } # } # return self.privatePostAccountSetPositionMode(self.extend(request, params)) 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 # # {"code":"152002","msg":"Parameter bar error."} # code = self.safe_string(response, 'code') message = self.safe_string(response, 'msg') feedback = self.id + ' ' + body if code is not None and code != '0': self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) raise ExchangeError(feedback) # unknown message # # { # orderId: null, # clientOrderId: '', # msg: 'Order failed. Insufficient USDT margin in account', # code: '103003' # } # data = self.safe_list(response, 'data') first = self.safe_dict(data, 0) insideMsg = self.safe_string(first, 'msg') insideCode = self.safe_string(first, 'code') if insideCode is not None and insideCode != '0': self.throw_exactly_matched_exception(self.exceptions['exact'], insideCode, feedback) self.throw_exactly_matched_exception(self.exceptions['exact'], insideMsg, feedback) self.throw_broadly_matched_exception(self.exceptions['broad'], insideMsg, feedback) return None def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): request = '/api/' + self.version + '/' + self.implode_params(path, params) query = self.omit(params, self.extract_params(path)) url = self.implode_hostname(self.urls['api']['rest']) + request # type = self.getPathAuthenticationType(path) if api == 'public': if not self.is_empty(query): url += '?' + self.urlencode(query) elif api == 'private': self.check_required_credentials() timestamp = str(self.milliseconds()) headers = { 'ACCESS-KEY': self.apiKey, 'ACCESS-PASSPHRASE': self.password, 'ACCESS-TIMESTAMP': timestamp, 'ACCESS-NONCE': timestamp, } sign_body = '' if method == 'GET': if not self.is_empty(query): urlencodedQuery = '?' + self.urlencode(query) url += urlencodedQuery request += urlencodedQuery else: if not self.is_empty(query): body = self.json(query) sign_body = body headers['Content-Type'] = 'application/json' auth = request + method + timestamp + timestamp + sign_body signature = self.string_to_base64(self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)) headers['ACCESS-SIGN'] = signature return {'url': url, 'method': method, 'body': body, 'headers': headers}