1936 lines
84 KiB
Python
1936 lines
84 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.async_support.base.exchange import Exchange
|
|
from ccxt.abstract.foxbit import ImplicitAPI
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction
|
|
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 AccountSuspended
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
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.errors import OnMaintenance
|
|
from ccxt.base.decimal_to_precision import DECIMAL_PLACES
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class foxbit(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(foxbit, self).describe(), {
|
|
'id': 'foxbit',
|
|
'name': 'Foxbit',
|
|
'countries': ['pt-BR'],
|
|
# 300 requests per 10 seconds = 30 requests per second
|
|
# rateLimit = 1000 ms / 30 requests ~= 33.334
|
|
'rateLimit': 33.334,
|
|
'version': '1',
|
|
'comment': 'Foxbit Exchange',
|
|
'certified': False,
|
|
'pro': False,
|
|
'has': {
|
|
'CORS': True,
|
|
'spot': True,
|
|
'margin': None,
|
|
'swap': None,
|
|
'future': None,
|
|
'option': None,
|
|
'cancelAllOrders': True,
|
|
'cancelOrder': True,
|
|
'createLimitBuyOrder': True,
|
|
'createLimitSellOrder': True,
|
|
'createMarketBuyOrder': True,
|
|
'createMarketSellOrder': True,
|
|
'createOrder': True,
|
|
'fecthOrderBook': True,
|
|
'fetchBalance': True,
|
|
'fetchCanceledOrders': True,
|
|
'fetchClosedOrders': True,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': True,
|
|
'fetchDeposits': True,
|
|
'fetchL2OrderBook': True,
|
|
'fetchLedger': True,
|
|
'fetchMarkets': True,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrders': True,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': True,
|
|
'fetchTradingFees': True,
|
|
'fetchTransactions': True,
|
|
'fetchWithdrawals': True,
|
|
'loadMarkets': True,
|
|
'sandbox': False,
|
|
'withdraw': True,
|
|
'ws': False,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'1h': '1h',
|
|
'2h': '2h',
|
|
'4h': '4h',
|
|
'6h': '6h',
|
|
'12h': '12h',
|
|
'1d': '1d',
|
|
'1w': '1w',
|
|
'2w': '2w',
|
|
'1M': '1M',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://github.com/user-attachments/assets/1f8faca2-ae2f-4222-b33e-5671e7d873dd',
|
|
'api': {
|
|
'public': 'https://api.foxbit.com.br',
|
|
'private': 'https://api.foxbit.com.br',
|
|
'status': 'https://metadata-v2.foxbit.com.br/api',
|
|
},
|
|
'www': 'https://app.foxbit.com.br',
|
|
'doc': [
|
|
'https://docs.foxbit.com.br',
|
|
],
|
|
},
|
|
'precisionMode': DECIMAL_PLACES,
|
|
'exceptions': {
|
|
'exact': {
|
|
# https://docs.foxbit.com.br/rest/v3/#tag/API-Codes/Errors
|
|
'400': BadRequest, # Bad request. An unknown error occurred while processing request parameters.
|
|
'429': RateLimitExceeded, # Too many requests. Request limit exceeded. Try again later.
|
|
'404': BadRequest, # Resource not found. A resource was not found while processing the request.
|
|
'500': ExchangeError, # Internal server error. An unknown error occurred while processing the request.
|
|
'2001': AuthenticationError, # Authentication error. Error authenticating request.
|
|
'2002': AuthenticationError, # Invalid signature. The signature for self request is not valid.
|
|
'2003': AuthenticationError, # Invalid access key. Access key missing, invalid or not found.
|
|
'2004': BadRequest, # Invalid timestamp. Invalid or missing timestamp.
|
|
'2005': PermissionDenied, # IP not allowed. The IP address {IP_ADDR} isn't on the trusted list for self API key.
|
|
'3001': PermissionDenied, # Permission denied. Permission denied for self request.
|
|
'3002': PermissionDenied, # KYC required. A greater level of KYC verification is required to proceed with self request.
|
|
'3003': AccountSuspended, # Member disabled. This member is disabled. Please get in touch with our support for more information.
|
|
'4001': BadRequest, # Validation error. A validation error occurred.
|
|
'4002': InsufficientFunds, # Insufficient funds. Insufficient funds to proceed with self request.
|
|
'4003': InvalidOrder, # Quantity below the minimum allowed. Quantity below the minimum allowed to proceed with self request.
|
|
'4004': BadSymbol, # Invalid symbol. The market or asset symbol is invalid or was not found.
|
|
'4005': BadRequest, # Invalid idempotent. Characters allowed are "a-z", "0-9", "_" or "-", and 36 at max. We recommend UUID v4 in lowercase.
|
|
'4007': ExchangeError, # Locked error. There was an error in your allocated balance, please contact us.
|
|
'4008': InvalidOrder, # Cannot submit order. The order cannot be created.
|
|
'4009': PermissionDenied, # Invalid level. The sub-member does not have the required level to create the transaction.
|
|
'4011': RateLimitExceeded, # Too many open orders. You have reached the limit of open orders per market/side.
|
|
'4012': ExchangeError, # Too many simultaneous account operations. We are currently unable to process your balance change due to simultaneous operations on your account. Please retry shortly.
|
|
'5001': ExchangeNotAvailable, # Service unavailable. The requested resource is currently unavailable. Try again later.
|
|
'5002': OnMaintenance, # Service under maintenance. The requested resource is currently under maintenance. Try again later.
|
|
'5003': OnMaintenance, # Market under maintenance. The market is under maintenance. Try again later.
|
|
'5004': InvalidOrder, # Market is not deep enough. The market is not deep enough to complete your request.
|
|
'5005': InvalidOrder, # Price out of range from market. The order price is out of range from market to complete your request.
|
|
'5006': InvalidOrder, # Significant price deviation detected, exceeding acceptable limits. The order price is exceeding acceptable limits from market to complete your request.
|
|
},
|
|
'broad': {
|
|
# todo: add details messages that can be usefull here, like when market is not found
|
|
},
|
|
},
|
|
'requiredCredentials': {
|
|
'apiKey': True,
|
|
'secret': True,
|
|
},
|
|
'api': {
|
|
'v3': {
|
|
'public': {
|
|
'get': {
|
|
'currencies': 5, # 6 requests per second
|
|
'markets': 5, # 6 requests per second
|
|
'markets/ticker/24hr': 60, # 1 request per 2 seconds
|
|
'markets/{market}/orderbook': 6, # 10 requests per 2 seconds
|
|
'markets/{market}/candlesticks': 12, # 5 requests per 2 seconds
|
|
'markets/{market}/trades/history': 12, # 5 requests per 2 seconds
|
|
'markets/{market}/ticker/24hr': 15, # 4 requests per 2 seconds
|
|
},
|
|
},
|
|
'private': {
|
|
'get': {
|
|
'accounts': 2, # 15 requests per second
|
|
'accounts/{symbol}/transactions': 60, # 1 requests per 2 seconds
|
|
'orders': 2, # 30 requests per 2 seconds
|
|
'orders/by-order-id/{id}': 2, # 30 requests per 2 seconds
|
|
'trades': 6, # 5 orders per second
|
|
'deposits/address': 10, # 3 requests per second
|
|
'deposits': 10, # 3 requests per second
|
|
'withdrawals': 10, # 3 requests per second
|
|
'me/fees/trading': 60, # 1 requests per 2 seconds
|
|
},
|
|
'post': {
|
|
'orders': 2, # 30 requests per 2 seconds
|
|
'orders/batch': 7.5, # 8 requests per 2 seconds
|
|
'orders/cancel-replace': 3, # 20 requests per 2 seconds
|
|
'withdrawals': 10, # 3 requests per second
|
|
},
|
|
'put': {
|
|
'orders/cancel': 2, # 30 requests per 2 seconds
|
|
},
|
|
},
|
|
},
|
|
'status': {
|
|
'public': {
|
|
'get': {
|
|
'status': 30, # 1 request per second
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'fees': {
|
|
'trading': {
|
|
'feeSide': 'get',
|
|
'tierBased': False,
|
|
'percentage': True,
|
|
'taker': self.parse_number('0.005'),
|
|
'maker': self.parse_number('0.0025'),
|
|
},
|
|
},
|
|
'options': {
|
|
'sandboxMode': False,
|
|
'networksById': {
|
|
'algorand': 'ALGO',
|
|
'arbitrum': 'ARBITRUM',
|
|
'avalanchecchain': 'AVAX',
|
|
'bitcoin': 'BTC',
|
|
'bitcoincash': 'BCH',
|
|
'bsc': 'BEP20',
|
|
'cardano': 'ADA',
|
|
'cosmos': 'ATOM',
|
|
'dogecoin': 'DOGE',
|
|
'erc20': 'ETH',
|
|
'hedera': 'HBAR',
|
|
'litecoin': 'LTC',
|
|
'near': 'NEAR',
|
|
'optimism': 'OPTIMISM',
|
|
'polkadot': 'DOT',
|
|
'polygon': 'MATIC',
|
|
'ripple': 'XRP',
|
|
'solana': 'SOL',
|
|
'stacks': 'STX',
|
|
'stellar': 'XLM',
|
|
'tezos': 'XTZ',
|
|
'trc20': 'TRC20',
|
|
},
|
|
'networks': {
|
|
'ALGO': 'algorand',
|
|
'ARBITRUM': 'arbitrum',
|
|
'AVAX': 'avalanchecchain',
|
|
'BTC': 'bitcoin',
|
|
'BCH': 'bitcoincash',
|
|
'BEP20': 'bsc',
|
|
'ADA': 'cardano',
|
|
'ATOM': 'cosmos',
|
|
'DOGE': 'dogecoin',
|
|
'ETH': 'erc20',
|
|
'HBAR': 'hedera',
|
|
'LTC': 'litecoin',
|
|
'NEAR': 'near',
|
|
'OPTIMISM': 'optimism',
|
|
'DOT': 'polkadot',
|
|
'MATIC': 'polygon',
|
|
'XRP': 'ripple',
|
|
'SOL': 'solana',
|
|
'STX': 'stacks',
|
|
'XLM': 'stellar',
|
|
'XTZ': 'tezos',
|
|
'TRC20': 'trc20',
|
|
},
|
|
},
|
|
'features': {
|
|
'spot': {
|
|
'sandbox': False,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': {
|
|
'last': True, # foxbit default trigger price type is last, no params will change it
|
|
'mark': False,
|
|
'index': False,
|
|
},
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False,
|
|
'takeProfitPrice': False,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'GTC': True,
|
|
'FOK': True,
|
|
'IOC': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'hedged': False,
|
|
'leverage': False,
|
|
'marketBuyByCost': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'selfTradePrevention': {
|
|
'expire_maker': True, # foxbit prevents self trading by default, no params can change self
|
|
'expire_taker': True, # foxbit prevents self trading by default, no params can change self
|
|
'expire_both': True, # foxbit prevents self trading by default, no params can change self
|
|
'none': True, # foxbit prevents self trading by default, no params can change self
|
|
},
|
|
'trailing': False,
|
|
'icebergAmount': False,
|
|
},
|
|
'createOrders': {
|
|
'max': 5,
|
|
},
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size
|
|
'symbolRequired': True,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'limit': 1,
|
|
'daysBack': 90,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': True,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': {
|
|
'marginMode': True,
|
|
'limit': 100,
|
|
'daysBack': 90,
|
|
'daysBackCanceled': 90,
|
|
'untilDays': 10000, # high value just to keep clear that there is no range limit, just the limit of the page size
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 500,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
async def fetch_currencies(self, params={}) -> Currencies:
|
|
response = await self.v3PublicGetCurrencies(params)
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "symbol": "btc",
|
|
# "name": "Bitcoin",
|
|
# "type": "CRYPTO",
|
|
# "precision": 8,
|
|
# "deposit_info": {
|
|
# "min_to_confirm": "1",
|
|
# "min_amount": "0.0001"
|
|
# },
|
|
# "withdraw_info": {
|
|
# "enabled": True,
|
|
# "min_amount": "0.0001",
|
|
# "fee": "0.0001"
|
|
# },
|
|
# "category": {
|
|
# "code": "cripto",
|
|
# "name": "Cripto"
|
|
# },
|
|
# "networks": [
|
|
# {
|
|
# "name": "Bitcoin",
|
|
# "code": "btc",
|
|
# "deposit_info": {
|
|
# status: "ENABLED",
|
|
# },
|
|
# "withdraw_info": {
|
|
# "status": "ENABLED",
|
|
# "fee": "0.0001",
|
|
# },
|
|
# "has_destination_tag": False
|
|
# }
|
|
# ]
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
result: dict = {}
|
|
for i in range(0, len(data)):
|
|
currency = data[i]
|
|
precision = self.safe_integer(currency, 'precision')
|
|
currencyId = self.safe_string(currency, 'symbol')
|
|
name = self.safe_string(currency, 'name')
|
|
code = self.safe_currency_code(currencyId)
|
|
depositInfo = self.safe_dict(currency, 'deposit_info')
|
|
withdrawInfo = self.safe_dict(currency, 'withdraw_info')
|
|
networks = self.safe_list(currency, 'networks', [])
|
|
type = self.safe_string_lower(currency, 'type')
|
|
parsedNetworks: dict = {}
|
|
for j in range(0, len(networks)):
|
|
network = networks[j]
|
|
networkId = self.safe_string(network, 'code')
|
|
networkCode = self.network_id_to_code(networkId, code)
|
|
networkWithdrawInfo = self.safe_dict(network, 'withdraw_info')
|
|
networkDepositInfo = self.safe_dict(network, 'deposit_info')
|
|
isWithdrawEnabled = self.safe_string(networkWithdrawInfo, 'status') == 'ENABLED'
|
|
isDepositEnabled = self.safe_string(networkDepositInfo, 'status') == 'ENABLED'
|
|
parsedNetworks[networkCode] = {
|
|
'info': currency,
|
|
'id': networkId,
|
|
'network': networkCode,
|
|
'name': self.safe_string(network, 'name'),
|
|
'deposit': isDepositEnabled,
|
|
'withdraw': isWithdrawEnabled,
|
|
'active': True,
|
|
'precision': precision,
|
|
'fee': self.safe_number(networkWithdrawInfo, 'fee'),
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': self.safe_number(depositInfo, 'min_amount'),
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': self.safe_number(withdrawInfo, 'min_amount'),
|
|
'max': None,
|
|
},
|
|
},
|
|
}
|
|
if self.safe_dict(result, code) is None:
|
|
result[code] = self.safe_currency_structure({
|
|
'id': currencyId,
|
|
'code': code,
|
|
'info': currency,
|
|
'name': name,
|
|
'active': True,
|
|
'type': type,
|
|
'deposit': self.safe_bool(depositInfo, 'enabled', False),
|
|
'withdraw': self.safe_bool(withdrawInfo, 'enabled', False),
|
|
'fee': self.safe_number(withdrawInfo, 'fee'),
|
|
'precision': precision,
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': self.safe_number(depositInfo, 'min_amount'),
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': self.safe_number(withdrawInfo, 'min_amount'),
|
|
'max': None,
|
|
},
|
|
},
|
|
'networks': parsedNetworks,
|
|
})
|
|
return result
|
|
|
|
async def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
Retrieves data on all markets for foxbit.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_index
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = await self.v3PublicGetMarkets(params)
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "symbol": "btcbrl",
|
|
# "quantity_min": "0.00000236",
|
|
# "quantity_increment": "0.00000001",
|
|
# "quantity_precision": 8,
|
|
# "price_min": "0.0001",
|
|
# "price_increment": "0.0001",
|
|
# "price_precision": 4,
|
|
# "default_fees": {
|
|
# "maker": "0.001",
|
|
# "taker": "0.001"
|
|
# },
|
|
# "base": {
|
|
# "symbol": "btc",
|
|
# "name": "Bitcoin",
|
|
# "type": "CRYPTO",
|
|
# "precision": 8,
|
|
# "category": {
|
|
# "code": "cripto",
|
|
# "name": "Cripto"
|
|
# },
|
|
# "deposit_info": {
|
|
# "min_to_confirm": "1",
|
|
# "min_amount": "0.0001",
|
|
# "enabled": True
|
|
# },
|
|
# "withdraw_info": {
|
|
# "enabled": True,
|
|
# "min_amount": "0.0001",
|
|
# "fee": "0.0001"
|
|
# },
|
|
# "networks": [
|
|
# {
|
|
# "name": "Bitcoin",
|
|
# "code": "bitcoin",
|
|
# "deposit_info": {
|
|
# "status": "ENABLED"
|
|
# },
|
|
# "withdraw_info": {
|
|
# "status": "ENABLED",
|
|
# "fee": "0.0001"
|
|
# },
|
|
# "has_destination_tag": False
|
|
# }
|
|
# ],
|
|
# "default_network_code": "bitcoin"
|
|
# },
|
|
# "quote": {
|
|
# "symbol": "btc",
|
|
# "name": "Bitcoin",
|
|
# "type": "CRYPTO",
|
|
# "precision": 8,
|
|
# "category": {
|
|
# "code": "cripto",
|
|
# "name": "Cripto"
|
|
# },
|
|
# "deposit_info": {
|
|
# "min_to_confirm": "1",
|
|
# "min_amount": "0.0001",
|
|
# "enabled": True
|
|
# },
|
|
# "withdraw_info": {
|
|
# "enabled": True,
|
|
# "min_amount": "0.0001",
|
|
# "fee": "0.0001"
|
|
# },
|
|
# "networks": [
|
|
# {
|
|
# "name": "Bitcoin",
|
|
# "code": "bitcoin",
|
|
# "deposit_info": {
|
|
# "status": "ENABLED"
|
|
# },
|
|
# "withdraw_info": {
|
|
# "status": "ENABLED",
|
|
# "fee": "0.0001"
|
|
# },
|
|
# "has_destination_tag": False
|
|
# }
|
|
# ],
|
|
# "default_network_code": "bitcoin"
|
|
# },
|
|
# "order_type": [
|
|
# "LIMIT",
|
|
# "MARKET",
|
|
# "INSTANT",
|
|
# "STOP_LIMIT",
|
|
# "STOP_MARKET"
|
|
# ]
|
|
# }
|
|
# ]
|
|
# }
|
|
markets = self.safe_list(response, 'data', [])
|
|
return self.parse_markets(markets)
|
|
|
|
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
Get last 24 hours ticker information, in real-time, for given market.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_ticker
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
}
|
|
response = await self.v3PublicGetMarketsMarketTicker24hr(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "market_symbol": "btcbrl",
|
|
# "last_trade": {
|
|
# "price": "358504.69340000",
|
|
# "volume": "0.00027893",
|
|
# "date": "2024-01-01T00:00:00.000Z"
|
|
# },
|
|
# "rolling_24h": {
|
|
# "price_change": "3211.87290000",
|
|
# "price_change_percent": "0.90400726",
|
|
# "volume": "20.03206866",
|
|
# "trades_count": "4376",
|
|
# "open": "355292.82050000",
|
|
# "high": "362999.99990000",
|
|
# "low": "355002.88880000"
|
|
# },
|
|
# "best": {
|
|
# "ask": {
|
|
# "price": "358504.69340000",
|
|
# "volume": "0.00027893"
|
|
# },
|
|
# "bid": {
|
|
# "price": "358504.69340000",
|
|
# "volume": "0.00027893"
|
|
# }
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
result = self.safe_dict(data, 0, {})
|
|
return self.parse_ticker(result, market)
|
|
|
|
async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
Retrieve the ticker data of all markets.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_tickers
|
|
|
|
:param str[]|None 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 <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
response = await self.v3PublicGetMarketsTicker24hr(params)
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "market_symbol": "btcbrl",
|
|
# "last_trade": {
|
|
# "price": "358504.69340000",
|
|
# "volume": "0.00027893",
|
|
# "date": "2024-01-01T00:00:00.000Z"
|
|
# },
|
|
# "rolling_24h": {
|
|
# "price_change": "3211.87290000",
|
|
# "price_change_percent": "0.90400726",
|
|
# "volume": "20.03206866",
|
|
# "trades_count": "4376",
|
|
# "open": "355292.82050000",
|
|
# "high": "362999.99990000",
|
|
# "low": "355002.88880000"
|
|
# },
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_tickers(data, symbols)
|
|
|
|
async def fetch_trading_fees(self, params={}) -> TradingFees:
|
|
"""
|
|
fetch the trading fees for multiple markets
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Member-Info/operation/MembersController_listTradingFees
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.v3PrivateGetMeFeesTrading(params)
|
|
# [
|
|
# {
|
|
# "market_symbol": "btcbrl",
|
|
# "maker": "0.0025",
|
|
# "taker": "0.005"
|
|
# }
|
|
# ]
|
|
data = self.safe_list(response, 'data', [])
|
|
result = {}
|
|
for i in range(0, len(data)):
|
|
entry = data[i]
|
|
marketId = self.safe_string(entry, 'market_symbol')
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
result[symbol] = self.parse_trading_fee(entry, market)
|
|
return result
|
|
|
|
async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
Exports a copy of the order book of a specific market.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findOrderbook
|
|
|
|
: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, the maximum is 100
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
defaultLimit = 20
|
|
request: dict = {
|
|
'market': market['id'],
|
|
'depth': defaultLimit if (limit is None) else limit,
|
|
}
|
|
response = await self.v3PublicGetMarketsMarketOrderbook(self.extend(request, params))
|
|
# {
|
|
# "sequence_id": 1234567890,
|
|
# "timestamp": 1713187921336,
|
|
# "bids": [
|
|
# [
|
|
# "3.00000000",
|
|
# "300.00000000"
|
|
# ],
|
|
# [
|
|
# "1.70000000",
|
|
# "310.00000000"
|
|
# ]
|
|
# ],
|
|
# "asks": [
|
|
# [
|
|
# "3.00000000",
|
|
# "300.00000000"
|
|
# ],
|
|
# [
|
|
# "2.00000000",
|
|
# "321.00000000"
|
|
# ]
|
|
# ]
|
|
# }
|
|
timestamp = self.safe_integer(response, 'timestamp')
|
|
return self.parse_order_book(response, symbol, timestamp)
|
|
|
|
async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
Retrieve the trades of a specific market.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_publicTrades
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 200:
|
|
request['page_size'] = 200
|
|
# [
|
|
# {
|
|
# "id": 1,
|
|
# "price": "329248.74700000",
|
|
# "volume": "0.00100000",
|
|
# "taker_side": "BUY",
|
|
# "created_at": "2024-01-01T00:00:00Z"
|
|
# }
|
|
# ]
|
|
response = await self.v3PublicGetMarketsMarketTradesHistory(self.extend(request, params))
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_trades(data, market, since, limit)
|
|
|
|
async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
Fetch historical candlestick data containing the open, high, low, and close price, and the volume of a market.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Market-Data/operation/MarketsController_findCandlesticks
|
|
|
|
: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
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
interval = self.safe_string(self.timeframes, timeframe, timeframe)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
'interval': interval,
|
|
}
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
if limit > 500:
|
|
request['limit'] = 500
|
|
response = await self.v3PublicGetMarketsMarketCandlesticks(self.extend(request, params))
|
|
# [
|
|
# [
|
|
# "1692918000000", # timestamp
|
|
# "127772.05150000", # open
|
|
# "128467.99980000", # high
|
|
# "127750.01000000", # low
|
|
# "128353.99990000", # close
|
|
# "1692918060000", # close timestamp
|
|
# "0.17080431", # base volume
|
|
# "21866.35948786", # quote volume
|
|
# 66, # number of trades
|
|
# "0.12073605", # taker buy base volume
|
|
# "15466.34096391" # taker buy quote volume
|
|
# ]
|
|
# ]
|
|
return self.parse_ohlcvs(response, market, interval, since, limit)
|
|
|
|
async def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
Query for balance and get the amount of funds available for trading or funds locked in orders.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_all
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.v3PrivateGetAccounts(params)
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "currency_symbol": "btc",
|
|
# "balance": "10000.0",
|
|
# "balance_available": "9000.0",
|
|
# "balance_locked": "1000.0"
|
|
# }
|
|
# ]
|
|
# }
|
|
accounts = self.safe_list(response, 'data', [])
|
|
result: dict = {
|
|
'info': response,
|
|
}
|
|
for i in range(0, len(accounts)):
|
|
account = accounts[i]
|
|
currencyId = self.safe_string(account, 'currency_symbol')
|
|
currencyCode = self.safe_currency_code(currencyId)
|
|
total = self.safe_string(account, 'balance')
|
|
used = self.safe_string(account, 'balance_locked')
|
|
free = self.safe_string(account, 'balance_available')
|
|
balanceObj = {
|
|
'free': free,
|
|
'used': used,
|
|
'total': total,
|
|
}
|
|
result[currencyCode] = balanceObj
|
|
return self.safe_balance(result)
|
|
|
|
async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
Fetch all unfilled currently open orders.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders
|
|
|
|
: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 order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status('ACTIVE', symbol, since, limit, params)
|
|
|
|
async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
Fetch all currently closed orders.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders
|
|
|
|
: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
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status('FILLED', symbol, since, limit, params)
|
|
|
|
async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
return await self.fetch_orders_by_status('CANCELED', symbol, since, limit, params)
|
|
|
|
async def fetch_orders_by_status(self, status: Str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
await self.load_markets()
|
|
market = None
|
|
request: dict = {
|
|
'state': status,
|
|
}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['market_symbol'] = market['id']
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
response = await self.v3PrivateGetOrders(self.extend(request, params))
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_orders(data)
|
|
|
|
async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
|
|
"""
|
|
Create an order with the specified characteristics
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_create
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market', 'limit', 'stop_market', 'stop_limit', 'instant'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much you want to trade in units of the base currency
|
|
:param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.timeInForce]: "GTC", "FOK", "IOC", "PO"
|
|
:param float [params.triggerPrice]: The time in force for the order. One of GTC, FOK, IOC, PO. See .features or foxbit's doc to see more details.
|
|
:param bool [params.postOnly]: True or False whether the order is post-only
|
|
:param str [params.clientOrderId]: a unique identifier for the order
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
type = type.upper()
|
|
if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT':
|
|
raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.')
|
|
timeInForce = self.safe_string_upper(params, 'timeInForce')
|
|
postOnly = self.safe_bool(params, 'postOnly', False)
|
|
triggerPrice = self.safe_number(params, 'triggerPrice')
|
|
request: dict = {
|
|
'market_symbol': market['id'],
|
|
'side': side.upper(),
|
|
'type': type,
|
|
}
|
|
if type == 'STOP_MARKET' or type == 'STOP_LIMIT':
|
|
if triggerPrice is None:
|
|
raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.')
|
|
if timeInForce is not None:
|
|
if timeInForce == 'PO':
|
|
request['post_only'] = True
|
|
else:
|
|
request['time_in_force'] = timeInForce
|
|
if postOnly:
|
|
request['post_only'] = True
|
|
if triggerPrice is not None:
|
|
request['stop_price'] = self.price_to_precision(symbol, triggerPrice)
|
|
if type == 'INSTANT':
|
|
request['amount'] = self.price_to_precision(symbol, amount)
|
|
else:
|
|
request['quantity'] = self.amount_to_precision(symbol, amount)
|
|
if type == 'LIMIT' or type == 'STOP_LIMIT':
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
request['client_order_id'] = clientOrderId
|
|
params = self.omit(params, ['timeInForce', 'postOnly', 'triggerPrice', 'clientOrderId'])
|
|
response = await self.v3PrivatePostOrders(self.extend(request, params))
|
|
# {
|
|
# "id": 1234567890,
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "client_order_id": "451637946501"
|
|
# }
|
|
return self.parse_order(response, market)
|
|
|
|
async def create_orders(self, orders: List[OrderRequest], params={}):
|
|
"""
|
|
create a list of trade orders
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/createBatch
|
|
|
|
: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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
ordersRequests = []
|
|
for i in range(0, len(orders)):
|
|
order = self.safe_dict(orders, i)
|
|
symbol = self.safe_string(order, 'symbol')
|
|
market = self.market(symbol)
|
|
type = self.safe_string_upper(order, 'type')
|
|
orderParams = self.safe_dict(order, 'params', {})
|
|
if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'STOP_LIMIT' and type != 'INSTANT':
|
|
raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: limit, market, stop_market, stop_limit, instant.')
|
|
timeInForce = self.safe_string_upper(orderParams, 'timeInForce')
|
|
postOnly = self.safe_bool(orderParams, 'postOnly', False)
|
|
triggerPrice = self.safe_number(orderParams, 'triggerPrice')
|
|
request: dict = {
|
|
'market_symbol': market['id'],
|
|
'side': self.safe_string_upper(order, 'side'),
|
|
'type': type,
|
|
}
|
|
if type == 'STOP_MARKET' or type == 'STOP_LIMIT':
|
|
if triggerPrice is None:
|
|
raise InvalidOrder('Invalid order type: ' + type + '. Must have triggerPrice.')
|
|
if timeInForce is not None:
|
|
if timeInForce == 'PO':
|
|
request['post_only'] = True
|
|
else:
|
|
request['time_in_force'] = timeInForce
|
|
del orderParams['timeInForce']
|
|
if postOnly:
|
|
request['post_only'] = True
|
|
del orderParams['postOnly']
|
|
if triggerPrice is not None:
|
|
request['stop_price'] = self.price_to_precision(symbol, triggerPrice)
|
|
del orderParams['triggerPrice']
|
|
if type == 'INSTANT':
|
|
request['amount'] = self.price_to_precision(symbol, self.safe_string(order, 'amount'))
|
|
else:
|
|
request['quantity'] = self.amount_to_precision(symbol, self.safe_string(order, 'amount'))
|
|
if type == 'LIMIT' or type == 'STOP_LIMIT':
|
|
request['price'] = self.price_to_precision(symbol, self.safe_string(order, 'price'))
|
|
ordersRequests.append(self.extend(request, orderParams))
|
|
createOrdersRequest = {'data': ordersRequests}
|
|
response = await self.v3PrivatePostOrdersBatch(self.extend(createOrdersRequest, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "side": "BUY",
|
|
# "type": "LIMIT",
|
|
# "market_symbol": "btcbrl",
|
|
# "client_order_id": "451637946501",
|
|
# "remark": "A remarkable note for the order.",
|
|
# "quantity": "0.42",
|
|
# "price": "250000.0",
|
|
# "post_only": True,
|
|
# "time_in_force": "GTC"
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_orders(data)
|
|
|
|
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
Cancel open orders.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel
|
|
|
|
: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
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'id': self.parse_number(id),
|
|
'type': 'ID',
|
|
}
|
|
response = await self.v3PrivatePutOrdersCancel(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "id": 123456789
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
result = self.safe_dict(data, 0, {})
|
|
return self.parse_order(result)
|
|
|
|
async def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
Cancel all open orders or all open orders for a specific market.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancel
|
|
|
|
:param str symbol: unified market symbol of the market to cancel orders in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'type': 'ALL',
|
|
}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['type'] = 'MARKET'
|
|
request['market_symbol'] = market['id']
|
|
response = await self.v3PrivatePutOrdersCancel(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "id": 123456789
|
|
# }
|
|
# ]
|
|
# }
|
|
return [self.safe_order({
|
|
'info': response,
|
|
})]
|
|
|
|
async def fetch_order(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
Get an order by ID.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_findByOrderId
|
|
|
|
@param id
|
|
:param str symbol: it is not used in the foxbit API
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'id': id,
|
|
}
|
|
response = await self.v3PrivateGetOrdersByOrderIdId(self.extend(request, params))
|
|
# {
|
|
# "id": "1234567890",
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "client_order_id": "451637946501",
|
|
# "market_symbol": "btcbrl",
|
|
# "side": "BUY",
|
|
# "type": "LIMIT",
|
|
# "state": "ACTIVE",
|
|
# "price": "290000.0",
|
|
# "price_avg": "295333.3333",
|
|
# "quantity": "0.42",
|
|
# "quantity_executed": "0.41",
|
|
# "instant_amount": "290.0",
|
|
# "instant_amount_executed": "290.0",
|
|
# "created_at": "2021-02-15T22:06:32.999Z",
|
|
# "trades_count": "2",
|
|
# "remark": "A remarkable note for the order.",
|
|
# "funds_received": "290.0"
|
|
# }
|
|
return self.parse_order(response, None)
|
|
|
|
async 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://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_listOrders
|
|
|
|
: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.state]: Enum: ACTIVE, CANCELED, FILLED, PARTIALLY_CANCELED, PARTIALLY_FILLED
|
|
:param str [params.side]: Enum: BUY, SELL
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
request: dict = {}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['market_symbol'] = market['id']
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
response = await self.v3PrivateGetOrders(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "id": "1234567890",
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "client_order_id": "451637946501",
|
|
# "market_symbol": "btcbrl",
|
|
# "side": "BUY",
|
|
# "type": "LIMIT",
|
|
# "state": "ACTIVE",
|
|
# "price": "290000.0",
|
|
# "price_avg": "295333.3333",
|
|
# "quantity": "0.42",
|
|
# "quantity_executed": "0.41",
|
|
# "instant_amount": "290.0",
|
|
# "instant_amount_executed": "290.0",
|
|
# "created_at": "2021-02-15T22:06:32.999Z",
|
|
# "trades_count": "2",
|
|
# "remark": "A remarkable note for the order.",
|
|
# "funds_received": "290.0"
|
|
# }
|
|
# ]
|
|
# }
|
|
list = self.safe_list(response, 'data', [])
|
|
return self.parse_orders(list, market, since, limit)
|
|
|
|
async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
Trade history queries will only have data available for the last 3 months, in descending order(most recents trades first).
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/TradesController_all
|
|
|
|
: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 trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request = {
|
|
'market_symbol': market['id'],
|
|
}
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
response = await self.v3PrivateGetTrades(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# "id": 1234567890,
|
|
# "sn": "TC5JZVW2LLJ3IW",
|
|
# "order_id": 1234567890,
|
|
# "market_symbol": "btcbrl",
|
|
# "side": "BUY",
|
|
# "price": "290000.0",
|
|
# "quantity": "1.0",
|
|
# "fee": "0.01",
|
|
# "fee_currency_symbol": "btc",
|
|
# "created_at": "2021-02-15T22:06:32.999Z"
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_trades(data, market, since, limit)
|
|
|
|
async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
Fetch the deposit address for a currency associated with self account.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_depositAddress
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.networkCode]: the blockchain network to create a deposit address on
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'currency_symbol': currency['id'],
|
|
}
|
|
networkCode, paramsOmited = self.handle_network_code_and_params(params)
|
|
if networkCode is not None:
|
|
request['network_code'] = self.network_code_to_id(networkCode, code)
|
|
response = await self.v3PrivateGetDepositsAddress(self.extend(request, paramsOmited))
|
|
# {
|
|
# "currency_symbol": "btc",
|
|
# "address": "2N9sS8LgrY19rvcCWDmE1ou1tTVmqk4KQAB",
|
|
# "message": "Address was retrieved successfully",
|
|
# "destination_tag": "string",
|
|
# "network": {
|
|
# "name": "Bitcoin Network",
|
|
# "code": "btc"
|
|
# }
|
|
# }
|
|
return self.parse_deposit_address(response, currency)
|
|
|
|
async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
Fetch all deposits made to an account.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders
|
|
|
|
: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 deposit structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
response = await self.v3PrivateGetDeposits(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "state": "ACCEPTED",
|
|
# "currency_symbol": "btc",
|
|
# "amount": "1.0",
|
|
# "fee": "0.1",
|
|
# "created_at": "2022-02-18T22:06:32.999Z",
|
|
# "details_crypto": {
|
|
# "transaction_id": "e20f035387020c5d5ea18ad53244f09f3",
|
|
# "receiving_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61"
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
Fetch all withdrawals made from an account.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals
|
|
|
|
: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 withdrawal structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
response = await self.v3PrivateGetWithdrawals(self.extend(request, params))
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "sn": "OKMAKSDHRVVREK",
|
|
# "state": "ACCEPTED",
|
|
# "rejection_reason": "monthly_limit_exceeded",
|
|
# "currency_symbol": "btc",
|
|
# "amount": "1.0",
|
|
# "fee": "0.1",
|
|
# "created_at": "2022-02-18T22:06:32.999Z",
|
|
# "details_crypto": {
|
|
# "transaction_id": "e20f035387020c5d5ea18ad53244f09f3",
|
|
# "destination_address": "2N2rTrnKEFcyJjEJqvVjgWZ3bKvKT7Aij61"
|
|
# },
|
|
# "details_fiat": {
|
|
# "bank": {
|
|
# "code": "1",
|
|
# "branch": {
|
|
# "number": "1234567890",
|
|
# "digit": "1"
|
|
# },
|
|
# "account": {
|
|
# "number": "1234567890",
|
|
# "digit": "1",
|
|
# "type": "CHECK"
|
|
# }
|
|
# }
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
async def fetch_transactions(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
Fetch all transactions(deposits and withdrawals) made from an account.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_listWithdrawals
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Deposit/operation/DepositsController_listOrders
|
|
|
|
: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 withdrawal structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
withdrawals = await self.fetch_withdrawals(code, since, limit, params)
|
|
deposits = await self.fetch_deposits(code, since, limit, params)
|
|
allTransactions = self.array_concat(withdrawals, deposits)
|
|
result = self.sort_by(allTransactions, 'timestamp')
|
|
return result
|
|
|
|
async def fetch_status(self, params={}):
|
|
"""
|
|
The latest known information on the availability of the exchange API.
|
|
|
|
https://status.foxbit.com/
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `status structure <https://docs.ccxt.com/#/?id=exchange-status-structure>`
|
|
"""
|
|
response = await self.statusPublicGetStatus(params)
|
|
# {
|
|
# "data": {
|
|
# "id": 1,
|
|
# "attributes": {
|
|
# "status": "NORMAL",
|
|
# "createdAt": "2023-05-17T18:37:05.934Z",
|
|
# "updatedAt": "2024-04-17T02:33:50.945Z",
|
|
# "publishedAt": "2023-05-17T18:37:07.653Z",
|
|
# "locale": "pt-BR"
|
|
# }
|
|
# },
|
|
# "meta": {
|
|
# }
|
|
# }
|
|
data = self.safe_dict(response, 'data', {})
|
|
attributes = self.safe_dict(data, 'attributes', {})
|
|
statusRaw = self.safe_string(attributes, 'status')
|
|
statusMap = {
|
|
'NORMAL': 'ok',
|
|
'UNDER_MAINTENANCE': 'maintenance',
|
|
}
|
|
return {
|
|
'status': self.safe_string(statusMap, statusRaw, statusRaw),
|
|
'updated': self.safe_string(attributes, 'updatedAt'),
|
|
'eta': None,
|
|
'url': None,
|
|
'info': response,
|
|
}
|
|
|
|
async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order:
|
|
"""
|
|
Simultaneously cancel an existing order and create a new one.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Trading/operation/OrdersController_cancelReplace
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of the currency you want to trade in units of the base currency
|
|
:param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders, used on stop market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' editOrder() requires a symbol argument')
|
|
type = type.upper()
|
|
if type != 'LIMIT' and type != 'MARKET' and type != 'STOP_MARKET' and type != 'INSTANT':
|
|
raise InvalidOrder('Invalid order type: ' + type + '. Must be one of: LIMIT, MARKET, STOP_MARKET, INSTANT.')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'mode': 'ALLOW_FAILURE',
|
|
'cancel': {
|
|
'type': 'ID',
|
|
'id': self.parse_number(id),
|
|
},
|
|
'create': {
|
|
'type': type,
|
|
'side': side.upper(),
|
|
'market_symbol': market['id'],
|
|
},
|
|
}
|
|
if type == 'LIMIT' or type == 'MARKET':
|
|
request['create']['quantity'] = self.amount_to_precision(symbol, amount)
|
|
if type == 'LIMIT':
|
|
request['create']['price'] = self.price_to_precision(symbol, price)
|
|
if type == 'STOP_MARKET':
|
|
request['create']['stop_price'] = self.price_to_precision(symbol, price)
|
|
request['create']['quantity'] = self.amount_to_precision(symbol, amount)
|
|
if type == 'INSTANT':
|
|
request['create']['amount'] = self.price_to_precision(symbol, amount)
|
|
response = await self.v3PrivatePostOrdersCancelReplace(self.extend(request, params))
|
|
# {
|
|
# "cancel": {
|
|
# "id": 123456789
|
|
# },
|
|
# "create": {
|
|
# "id": 1234567890,
|
|
# "client_order_id": "451637946501"
|
|
# }
|
|
# }
|
|
return self.parse_order(response['create'], market)
|
|
|
|
async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
Make a withdrawal.
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Withdrawal/operation/WithdrawalsController_createWithdrawal
|
|
|
|
: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
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
tag, params = self.handle_withdraw_tag_and_params(tag, params)
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'currency_symbol': currency['id'],
|
|
'amount': self.number_to_string(amount),
|
|
'destination_address': address,
|
|
}
|
|
if tag is not None:
|
|
request['destination_tag'] = tag
|
|
networkCode = None
|
|
networkCode, params = self.handle_network_code_and_params(params)
|
|
if networkCode is not None:
|
|
request['network_code'] = self.network_code_to_id(networkCode)
|
|
response = await self.v3PrivatePostWithdrawals(self.extend(request, params))
|
|
# {
|
|
# "amount": "1",
|
|
# "currency_symbol": "xrp",
|
|
# "network_code": "ripple",
|
|
# "destination_address": "0x1234567890123456789012345678",
|
|
# "destination_tag": "123456"
|
|
# }
|
|
return self.parse_transaction(response)
|
|
|
|
async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch the history of changes, actions done by the user or operations that altered balance of the user
|
|
|
|
https://docs.foxbit.com.br/rest/v3/#tag/Account/operation/AccountsController_getTransactions
|
|
|
|
: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 entrys to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
if code is None:
|
|
raise ArgumentsRequired(self.id + ' fetchLedger() requires a code argument')
|
|
if limit is not None:
|
|
request['page_size'] = limit
|
|
if limit > 100:
|
|
request['page_size'] = 100
|
|
if since is not None:
|
|
request['start_time'] = self.iso8601(since)
|
|
currency = self.currency(code)
|
|
request['symbol'] = currency['id']
|
|
response = await self.v3PrivateGetAccountsSymbolTransactions(self.extend(request, params))
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_ledger(data, currency, since, limit)
|
|
|
|
def parse_market(self, market: dict) -> Market:
|
|
id = self.safe_string(market, 'symbol')
|
|
baseAssets = self.safe_dict(market, 'base')
|
|
baseId = self.safe_string(baseAssets, 'symbol')
|
|
quoteAssets = self.safe_dict(market, 'quote')
|
|
quoteId = self.safe_string(quoteAssets, 'symbol')
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
symbol = base + '/' + quote
|
|
fees = self.safe_dict(market, 'default_fees')
|
|
return self.safe_market_structure({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'active': True,
|
|
'type': 'spot',
|
|
'spot': True,
|
|
'margin': False,
|
|
'future': False,
|
|
'swap': False,
|
|
'option': False,
|
|
'contract': False,
|
|
'settle': None,
|
|
'settleId': None,
|
|
'contractSize': None,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'taker': self.safe_number(fees, 'taker'),
|
|
'maker': self.safe_number(fees, 'maker'),
|
|
'percentage': True,
|
|
'tierBased': False,
|
|
'feeSide': 'get',
|
|
'precision': {
|
|
'price': self.safe_integer(quoteAssets, 'precision'),
|
|
'amount': self.safe_integer(baseAssets, 'precision'),
|
|
'cost': self.safe_integer(quoteAssets, 'precision'),
|
|
},
|
|
'limits': {
|
|
'amount': {
|
|
'min': self.safe_number(market, 'quantity_min'),
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': self.safe_number(market, 'price_min'),
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'info': market,
|
|
})
|
|
|
|
def parse_trading_fee(self, entry: dict, market: Market = None) -> TradingFeeInterface:
|
|
return {
|
|
'info': entry,
|
|
'symbol': market['symbol'],
|
|
'maker': self.safe_number(entry, 'maker'),
|
|
'taker': self.safe_number(entry, 'taker'),
|
|
'percentage': True,
|
|
'tierBased': True,
|
|
}
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
marketId = self.safe_string(ticker, 'market_symbol')
|
|
symbol = self.safe_symbol(marketId, market, None, 'spot')
|
|
rolling_24h = ticker['rolling_24h']
|
|
best = self.safe_dict(ticker, 'best')
|
|
bestAsk = self.safe_dict(best, 'ask')
|
|
bestBid = self.safe_dict(best, 'bid')
|
|
lastTrade = ticker['last_trade']
|
|
lastPrice = self.safe_string(lastTrade, 'price')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': self.parse_date(self.safe_string(lastTrade, 'date')),
|
|
'datetime': self.iso8601(self.parse_date(self.safe_string(lastTrade, 'date'))),
|
|
'high': self.safe_number(rolling_24h, 'high'),
|
|
'low': self.safe_number(rolling_24h, 'low'),
|
|
'bid': self.safe_number(bestBid, 'price'),
|
|
'bidVolume': self.safe_number(bestBid, 'volume'),
|
|
'ask': self.safe_number(bestAsk, 'price'),
|
|
'askVolume': self.safe_number(bestAsk, 'volume'),
|
|
'vwap': None,
|
|
'open': self.safe_number(rolling_24h, 'open'),
|
|
'close': lastPrice,
|
|
'last': lastPrice,
|
|
'previousClose': None,
|
|
'change': self.safe_string(rolling_24h, 'price_change'),
|
|
'percentage': self.safe_string(rolling_24h, 'price_change_percent'),
|
|
'average': None,
|
|
'baseVolume': self.safe_string(rolling_24h, 'volume'),
|
|
'quoteVolume': self.safe_string(rolling_24h, 'quote_volume'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
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 parse_trade(self, trade, market=None) -> Trade:
|
|
timestamp = self.parse_date(self.safe_string(trade, 'created_at'))
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'volume', self.safe_string(trade, 'quantity'))
|
|
privateSideField = self.safe_string_lower(trade, 'side')
|
|
side = self.safe_string_lower(trade, 'taker_side', privateSideField)
|
|
cost = Precise.string_mul(price, amount)
|
|
fee = {
|
|
'currency': self.safe_symbol(self.safe_string(trade, 'fee_currency_symbol')),
|
|
'cost': self.safe_number(trade, 'fee'),
|
|
'rate': None,
|
|
}
|
|
return self.safe_trade({
|
|
'id': self.safe_string(trade, 'id'),
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'order': None,
|
|
'type': None,
|
|
'side': side,
|
|
'takerOrMaker': None,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': cost,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'PARTIALLY_CANCELED': 'open',
|
|
'ACTIVE': 'open',
|
|
'PARTIALLY_FILLED': 'open',
|
|
'FILLED': 'closed',
|
|
'PENDING_CANCEL': 'canceled',
|
|
'CANCELED': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order(self, order, market=None) -> Order:
|
|
symbol = self.safe_string(order, 'market_symbol')
|
|
if market is None and symbol is not None:
|
|
market = self.market(symbol)
|
|
if market is not None:
|
|
symbol = market['symbol']
|
|
timestamp = self.parse_date(self.safe_string(order, 'created_at'))
|
|
price = self.safe_string(order, 'price')
|
|
filled = self.safe_string(order, 'quantity_executed')
|
|
remaining = self.safe_string(order, 'quantity')
|
|
# TODO: validate logic of amount here, should self be calculated?
|
|
amount = None
|
|
if remaining is not None and filled is not None:
|
|
amount = Precise.string_add(remaining, filled)
|
|
cost = self.safe_string(order, 'funds_received')
|
|
if not cost:
|
|
priceAverage = self.safe_string(order, 'price_avg')
|
|
priceToCalculate = self.safe_string(order, 'price', priceAverage)
|
|
cost = Precise.string_mul(priceToCalculate, amount)
|
|
side = self.safe_string_lower(order, 'side')
|
|
feeCurrency = self.safe_string_upper(market, 'quoteId')
|
|
if side == 'buy':
|
|
feeCurrency = self.safe_string_upper(market, 'baseId')
|
|
return self.safe_order({
|
|
'id': self.safe_string(order, 'id'),
|
|
'info': order,
|
|
'clientOrderId': self.safe_string(order, 'client_order_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'status': self.parse_order_status(self.safe_string(order, 'state')),
|
|
'symbol': self.safe_string(market, 'symbol'),
|
|
'type': self.safe_string(order, 'type'),
|
|
'timeInForce': self.safe_string(order, 'time_in_force'),
|
|
'postOnly': self.safe_bool(order, 'post_only'),
|
|
'reduceOnly': None,
|
|
'side': side,
|
|
'price': self.parse_number(price),
|
|
'triggerPrice': self.safe_number(order, 'stop_price'),
|
|
'takeProfitPrice': None,
|
|
'stopLossPrice': None,
|
|
'cost': self.parse_number(cost),
|
|
'average': self.safe_number(order, 'price_avg'),
|
|
'amount': self.parse_number(amount),
|
|
'filled': self.parse_number(filled),
|
|
'remaining': self.parse_number(remaining),
|
|
'trades': None,
|
|
'fee': {
|
|
'currency': feeCurrency,
|
|
'cost': self.safe_number(order, 'fee_paid'),
|
|
},
|
|
})
|
|
|
|
def parse_deposit_address(self, depositAddress, currency: Currency = None):
|
|
network = self.safe_dict(depositAddress, 'network')
|
|
networkId = self.safe_string(network, 'code')
|
|
currencyCode = self.safe_currency_code(None, currency)
|
|
unifiedNetwork = self.network_id_to_code(networkId, currencyCode)
|
|
return {
|
|
'address': self.safe_string(depositAddress, 'address'),
|
|
'tag': self.safe_string(depositAddress, 'tag'),
|
|
'currency': currencyCode,
|
|
'network': unifiedNetwork,
|
|
'info': depositAddress,
|
|
}
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
# BOTH
|
|
'SUBMITTING': 'pending',
|
|
'SUBMITTED': 'pending',
|
|
'REJECTED': 'failed',
|
|
# DEPOSIT-SPECIFIC
|
|
'CANCELLED': 'canceled',
|
|
'ACCEPTED': 'ok',
|
|
'WARNING': 'pending',
|
|
'UNBLOCKED': 'pending',
|
|
'BLOCKED': 'pending',
|
|
# WITHDRAWAL-SPECIFIC
|
|
'PROCESSING': 'pending',
|
|
'CANCELED': 'canceled',
|
|
'FAILED': 'failed',
|
|
'DONE': 'ok',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_transaction(self, transaction, currency: Currency = None, since: Int = None, limit: Int = None) -> Transaction:
|
|
cryptoDetails = self.safe_dict(transaction, 'details_crypto')
|
|
address = self.safe_string_2(cryptoDetails, 'receiving_address', 'destination_address')
|
|
sn = self.safe_string(transaction, 'sn')
|
|
type = 'withdrawal'
|
|
if sn is not None and sn[0] == 'D':
|
|
type = 'deposit'
|
|
fee = self.safe_string(transaction, 'fee', '0')
|
|
amount = self.safe_string(transaction, 'amount')
|
|
currencySymbol = self.safe_string(transaction, 'currency_symbol')
|
|
actualAmount = amount
|
|
currencyCode = self.safe_currency_code(currencySymbol)
|
|
status = self.parse_transaction_status(self.safe_string(transaction, 'state'))
|
|
created_at = self.safe_string(transaction, 'created_at')
|
|
timestamp = self.parse_date(created_at)
|
|
datetime = self.iso8601(timestamp)
|
|
if fee is not None and amount is not None:
|
|
# actualAmount = amount - fee
|
|
actualAmount = Precise.string_sub(amount, fee)
|
|
feeRate = Precise.string_div(fee, actualAmount)
|
|
feeObj = {
|
|
'cost': self.parse_number(fee),
|
|
'currency': currencyCode,
|
|
'rate': self.parse_number(feeRate),
|
|
}
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string(transaction, 'sn'),
|
|
'txid': self.safe_string(cryptoDetails, 'transaction_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': datetime,
|
|
'network': self.safe_string(transaction, 'network_code'),
|
|
'address': address,
|
|
'addressTo': address,
|
|
'addressFrom': None,
|
|
'tag': self.safe_string(transaction, 'destination_tag'),
|
|
'tagTo': self.safe_string(transaction, 'destination_tag'),
|
|
'tagFrom': None,
|
|
'type': type,
|
|
'amount': self.parse_number(amount),
|
|
'currency': currencyCode,
|
|
'status': status,
|
|
'updated': None,
|
|
'fee': feeObj,
|
|
'comment': None,
|
|
'internal': None,
|
|
}
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
types: dict = {
|
|
'DEPOSITING': 'transaction',
|
|
'WITHDRAWING': 'transaction',
|
|
'TRADING': 'trade',
|
|
'INTERNAL_TRANSFERING': 'transfer',
|
|
'OTHERS': 'transaction',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None):
|
|
# {
|
|
# "uuid": "f8e9f2d6-3c1e-4f2d-8f8e-9f2d6c1e4f2d",
|
|
# "amount": "0.0001",
|
|
# "balance": "0.0002",
|
|
# "created_at": "2021-07-01T12:00:00Z",
|
|
# "currency_symbol": "btc",
|
|
# "fee": "0.0001",
|
|
# "locked": "0.0001",
|
|
# "locked_amount": "0.0001",
|
|
# "reason_type": "DEPOSITING"
|
|
# }
|
|
id = self.safe_string(item, 'uuid')
|
|
createdAt = self.safe_string(item, 'created_at')
|
|
timestamp = self.parse8601(createdAt)
|
|
reasonType = self.safe_string(item, 'reason_type')
|
|
type = self.parse_ledger_entry_type(reasonType)
|
|
exchangeSymbol = self.safe_string(item, 'currency_symbol')
|
|
currencySymbol = self.safe_currency_code(exchangeSymbol)
|
|
direction = 'in'
|
|
amount = self.safe_number(item, 'amount')
|
|
realAmount = amount
|
|
balance = self.safe_number(item, 'balance')
|
|
fee = {
|
|
'cost': self.safe_number(item, 'fee'),
|
|
'currency': currencySymbol,
|
|
}
|
|
if amount < 0:
|
|
direction = 'out'
|
|
realAmount = amount * -1
|
|
return {
|
|
'id': id,
|
|
'info': item,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'direction': direction,
|
|
'account': None,
|
|
'referenceId': None,
|
|
'referenceAccount': None,
|
|
'type': type,
|
|
'currency': currencySymbol,
|
|
'amount': realAmount,
|
|
'before': balance - amount,
|
|
'after': balance,
|
|
'status': 'ok',
|
|
'fee': fee,
|
|
}
|
|
|
|
def sign(self, path, api=[], method='GET', params={}, headers=None, body=None):
|
|
version = api[0]
|
|
urlPath = api[1]
|
|
fullPath = '/rest/' + version + '/' + self.implode_params(path, params)
|
|
if version == 'status':
|
|
fullPath = '/status'
|
|
urlPath = 'status'
|
|
url = self.urls['api'][urlPath] + fullPath
|
|
params = self.omit(params, self.extract_params(path))
|
|
timestamp = self.milliseconds()
|
|
query = ''
|
|
signatureQuery = ''
|
|
if method == 'GET':
|
|
paramKeys = list(params.keys())
|
|
paramKeysLength = len(paramKeys)
|
|
if paramKeysLength > 0:
|
|
query = self.urlencode(params)
|
|
url += '?' + query
|
|
for i in range(0, len(paramKeys)):
|
|
key = paramKeys[i]
|
|
value = self.safe_string(params, key)
|
|
if value is not None:
|
|
signatureQuery += key + '=' + value
|
|
if i < paramKeysLength - 1:
|
|
signatureQuery += '&'
|
|
if method == 'POST' or method == 'PUT':
|
|
body = self.json(params)
|
|
bodyToSignature = ''
|
|
if body is not None:
|
|
bodyToSignature = body
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
if urlPath == 'private':
|
|
self.check_required_credentials()
|
|
preHash = self.number_to_string(timestamp) + method + fullPath + signatureQuery + bodyToSignature
|
|
signature = self.hmac(self.encode(preHash), self.encode(self.secret), hashlib.sha256, 'hex')
|
|
headers['X-FB-ACCESS-KEY'] = self.apiKey
|
|
headers['X-FB-ACCESS-TIMESTAMP'] = self.number_to_string(timestamp)
|
|
headers['X-FB-ACCESS-SIGNATURE'] = signature
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
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
|
|
error = self.safe_dict(response, 'error')
|
|
code = self.safe_string(error, 'code')
|
|
details = self.safe_list(error, 'details')
|
|
message = self.safe_string(error, 'message')
|
|
detailsString = ''
|
|
if details:
|
|
for i in range(0, len(details)):
|
|
detailsString = detailsString + details[i] + ' '
|
|
if error is not None:
|
|
feedback = self.id + ' ' + message + ' details: ' + detailsString
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], detailsString, feedback)
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
raise ExchangeError(feedback)
|
|
return None
|