2049 lines
89 KiB
Python
2049 lines
89 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.coinbaseexchange import ImplicitAPI
|
|
import hashlib
|
|
from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, 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 ArgumentsRequired
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidAddress
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.errors import OnMaintenance
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class coinbaseexchange(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(coinbaseexchange, self).describe(), {
|
|
'id': 'coinbaseexchange',
|
|
'name': 'Coinbase Exchange',
|
|
'countries': ['US'],
|
|
'rateLimit': 100,
|
|
'userAgent': self.userAgents['chrome'],
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': True,
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'borrowCrossMargin': False,
|
|
'borrowIsolatedMargin': False,
|
|
'borrowMargin': False,
|
|
'cancelAllOrders': True,
|
|
'cancelOrder': True,
|
|
'closeAllPositions': False,
|
|
'closePosition': False,
|
|
'createDepositAddress': True,
|
|
'createOrder': True,
|
|
'createOrderWithTakeProfitAndStopLoss': False,
|
|
'createOrderWithTakeProfitAndStopLossWs': False,
|
|
'createPostOnlyOrder': False,
|
|
'createReduceOnlyOrder': False,
|
|
'createStopLimitOrder': True,
|
|
'createStopMarketOrder': True,
|
|
'createStopOrder': True,
|
|
'fetchAccounts': True,
|
|
'fetchBalance': True,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRate': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': False,
|
|
'fetchBorrowRates': False,
|
|
'fetchBorrowRatesPerSymbol': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': False, # the exchange does not have self method, only createDepositAddress, see https://github.com/ccxt/ccxt/pull/7405
|
|
'fetchDeposits': True,
|
|
'fetchDepositsWithdrawals': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingInterval': False,
|
|
'fetchFundingIntervals': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': False,
|
|
'fetchGreeks': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchIsolatedPositions': False,
|
|
'fetchLedger': True,
|
|
'fetchLeverage': False,
|
|
'fetchLeverages': False,
|
|
'fetchLeverageTiers': False,
|
|
'fetchLiquidations': False,
|
|
'fetchLongShortRatio': False,
|
|
'fetchLongShortRatioHistory': False,
|
|
'fetchMarginAdjustmentHistory': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarginModes': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMarkPrices': False,
|
|
'fetchMyLiquidations': False,
|
|
'fetchMySettlementHistory': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterest': False,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenInterests': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOption': False,
|
|
'fetchOptionChain': False,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': True,
|
|
'fetchOrderTrades': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': False,
|
|
'fetchPositionsForSymbol': False,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchSettlementHistory': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTime': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': False,
|
|
'fetchTradingFees': True,
|
|
'fetchTransactions': 'emulated',
|
|
'fetchVolatilityHistory': False,
|
|
'fetchWithdrawals': True,
|
|
'reduceMargin': False,
|
|
'repayCrossMargin': False,
|
|
'repayIsolatedMargin': False,
|
|
'repayMargin': False,
|
|
'setLeverage': False,
|
|
'setMargin': False,
|
|
'setMarginMode': False,
|
|
'setPositionMode': False,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': 60,
|
|
'5m': 300,
|
|
'15m': 900,
|
|
'1h': 3600,
|
|
'6h': 21600,
|
|
'1d': 86400,
|
|
},
|
|
'hostname': 'exchange.coinbase.com',
|
|
'urls': {
|
|
'test': {
|
|
'public': 'https://api-public.sandbox.exchange.coinbase.com',
|
|
'private': 'https://api-public.sandbox.exchange.coinbase.com',
|
|
},
|
|
'logo': 'https://github.com/ccxt/ccxt/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e',
|
|
'api': {
|
|
'public': 'https://api.{hostname}',
|
|
'private': 'https://api.{hostname}',
|
|
},
|
|
'www': 'https://coinbase.com/',
|
|
'doc': 'https://docs.cloud.coinbase.com/exchange/docs/',
|
|
'fees': [
|
|
'https://docs.pro.coinbase.com/#fees',
|
|
'https://support.pro.coinbase.com/customer/en/portal/articles/2945310-fees',
|
|
],
|
|
},
|
|
'requiredCredentials': {
|
|
'apiKey': True,
|
|
'secret': True,
|
|
'password': True,
|
|
},
|
|
'api': {
|
|
'public': {
|
|
'get': [
|
|
'currencies',
|
|
'products',
|
|
'products/{id}',
|
|
'products/{id}/book',
|
|
'products/{id}/candles',
|
|
'products/{id}/stats',
|
|
'products/{id}/ticker',
|
|
'products/{id}/trades',
|
|
'time',
|
|
'products/spark-lines', # experimental,
|
|
'products/volume-summary',
|
|
],
|
|
},
|
|
'private': {
|
|
'get': [
|
|
'address-book',
|
|
'accounts',
|
|
'accounts/{id}',
|
|
'accounts/{id}/holds',
|
|
'accounts/{id}/ledger',
|
|
'accounts/{id}/transfers',
|
|
'coinbase-accounts',
|
|
'fills',
|
|
'funding',
|
|
'fees',
|
|
'margin/profile_information',
|
|
'margin/buying_power',
|
|
'margin/withdrawal_power',
|
|
'margin/withdrawal_power_all',
|
|
'margin/exit_plan',
|
|
'margin/liquidation_history',
|
|
'margin/position_refresh_amounts',
|
|
'margin/status',
|
|
'oracle',
|
|
'orders',
|
|
'orders/{id}',
|
|
'orders/client:{client_oid}',
|
|
'otc/orders',
|
|
'payment-methods',
|
|
'position',
|
|
'profiles',
|
|
'profiles/{id}',
|
|
'reports/{report_id}',
|
|
'transfers',
|
|
'transfers/{transfer_id}',
|
|
'users/self/exchange-limits',
|
|
'users/self/hold-balances',
|
|
'users/self/trailing-volume',
|
|
'withdrawals/fee-estimate',
|
|
'conversions/{conversion_id}',
|
|
'conversions',
|
|
'conversions/fees',
|
|
'loans/lending-overview',
|
|
'loans/lending-overview-xm',
|
|
'loans/loan-preview',
|
|
'loans/loan-preview-xm',
|
|
'loans/repayment-preview',
|
|
'loans/repayment-preview-xm',
|
|
'loans/interest/{loan_id}',
|
|
'loans/interest/history/{loan_id}',
|
|
'loans/interest',
|
|
'loans/assets',
|
|
'loans',
|
|
],
|
|
'post': [
|
|
'conversions',
|
|
'deposits/coinbase-account',
|
|
'deposits/payment-method',
|
|
'coinbase-accounts/{id}/addresses',
|
|
'funding/repay',
|
|
'orders',
|
|
'position/close',
|
|
'profiles/margin-transfer',
|
|
'profiles/transfer',
|
|
'reports',
|
|
'withdrawals/coinbase',
|
|
'withdrawals/coinbase-account',
|
|
'withdrawals/crypto',
|
|
'withdrawals/payment-method',
|
|
'loans/open',
|
|
'loans/repay-interest',
|
|
'loans/repay-principal',
|
|
],
|
|
'delete': [
|
|
'orders',
|
|
'orders/client:{client_oid}',
|
|
'orders/{id}',
|
|
],
|
|
},
|
|
},
|
|
'commonCurrencies': {
|
|
'CGLD': 'CELO',
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'fees': {
|
|
'trading': {
|
|
'tierBased': True, # complicated tier system per coin
|
|
'percentage': True,
|
|
'maker': self.parse_number('0.004'), # highest fee of all tiers
|
|
'taker': self.parse_number('0.006'), # highest fee of all tiers
|
|
},
|
|
'funding': {
|
|
'tierBased': False,
|
|
'percentage': False,
|
|
'withdraw': {
|
|
'BCH': 0,
|
|
'BTC': 0,
|
|
'LTC': 0,
|
|
'ETH': 0,
|
|
'EUR': 0.15,
|
|
'USD': 25,
|
|
},
|
|
'deposit': {
|
|
'BCH': 0,
|
|
'BTC': 0,
|
|
'LTC': 0,
|
|
'ETH': 0,
|
|
'EUR': 0.15,
|
|
'USD': 10,
|
|
},
|
|
},
|
|
},
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': True,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': None,
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False, # todo
|
|
'takeProfitPrice': False, # todo
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': True,
|
|
},
|
|
'hedged': False,
|
|
'trailing': False,
|
|
'leverage': False,
|
|
'marketBuyByCost': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'selfTradePrevention': False,
|
|
'iceberg': True, # todo: implement
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 100000,
|
|
'untilDays': 100000,
|
|
'symbolRequired': True,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 100000,
|
|
'untilDays': 100000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 100,
|
|
'daysBack': 100000,
|
|
'daysBackCanceled': 1,
|
|
'untilDays': 100000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 300,
|
|
},
|
|
},
|
|
'spot': {
|
|
'extends': 'default',
|
|
},
|
|
'swap': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'options': {
|
|
'networks': {
|
|
'BTC': 'bitcoin',
|
|
# LIGHTNING unsupported
|
|
'ETH': 'ethereum',
|
|
# TRON unsupported
|
|
'SOL': 'solana',
|
|
# BSC unsupported
|
|
'ARBONE': 'arbitrum',
|
|
'AVAXC': 'avacchain',
|
|
'MATIC': 'polygon',
|
|
'BASE': 'base',
|
|
'SUI': 'sui',
|
|
'OP': 'optimism',
|
|
'NEAR': 'near',
|
|
# CRONOS unsupported
|
|
# GNO unsupported
|
|
'APT': 'aptos',
|
|
# SCROLL unsupported
|
|
'KAVA': 'kava',
|
|
# TAIKO unsupported
|
|
# BOB unsupported
|
|
# LINEA unsupported
|
|
'BLAST': 'blast',
|
|
'XLM': 'stellar',
|
|
# RSK unsupported
|
|
'SEI': 'sei',
|
|
# TON unsupported
|
|
# MANTLE unsupported
|
|
'ADA': 'cardano',
|
|
# HYPE unsupported
|
|
'CORE': 'coredao',
|
|
'ALGO': 'algorand',
|
|
# RUNE unsupported
|
|
'OSMO': 'osmosis',
|
|
# XIN unsupported
|
|
'CELO': 'celo',
|
|
'HBAR': 'hedera',
|
|
# FTM unsupported
|
|
# WEMIX unsupported
|
|
'ZKSYNC': 'zksync',
|
|
# KLAY unsupported
|
|
# HT unsupported
|
|
# FSN unsupported
|
|
# EOS unsupported, eosio?
|
|
# ACA unsupported
|
|
'STX': 'stacks',
|
|
'XTZ': 'tezos',
|
|
# NEO unsupported
|
|
# METIS unsupported
|
|
# TLOS unsupported
|
|
'EGLD': 'elrond',
|
|
# ASTR unsupported
|
|
# CFX unsupported
|
|
# GLMR unsupported
|
|
# CANTO unsupported
|
|
# SCRT unsupported
|
|
'LTC': 'litecoin',
|
|
# AURORA unsupported
|
|
# ONG unsupported
|
|
'ATOM': 'cosmos',
|
|
# CHZ unsupported
|
|
'FIL': 'filecoin',
|
|
'DOT': 'polkadot',
|
|
'DOGE': 'dogecoin',
|
|
# BRC20 unsupported
|
|
'XRP': 'ripple',
|
|
# XMR unsupported
|
|
'DASH': 'dash',
|
|
# akash, aleo, axelar, bitcoincash, berachain, deso, ethereumclassic, unichain, flow, flare, dfinity, story,kusama, mina, ronin, oasis, bittensor, celestia, noble, vara, vechain, zcash, horizen, zetachain
|
|
},
|
|
},
|
|
'exceptions': {
|
|
'exact': {
|
|
'Insufficient funds': InsufficientFunds,
|
|
'NotFound': OrderNotFound,
|
|
'Invalid API Key': AuthenticationError,
|
|
'invalid signature': AuthenticationError,
|
|
'Invalid Passphrase': AuthenticationError,
|
|
'Invalid order id': InvalidOrder,
|
|
'Private rate limit exceeded': RateLimitExceeded,
|
|
'Trading pair not available': PermissionDenied,
|
|
'Product not found': InvalidOrder,
|
|
},
|
|
'broad': {
|
|
'Order already done': OrderNotFound,
|
|
'order not found': OrderNotFound,
|
|
'price too small': InvalidOrder,
|
|
'price too precise': InvalidOrder,
|
|
'under maintenance': OnMaintenance,
|
|
'size is too small': InvalidOrder,
|
|
'Cancel only mode': OnMaintenance, # https://github.com/ccxt/ccxt/issues/7690
|
|
},
|
|
},
|
|
})
|
|
|
|
async def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getcurrencies
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
response = await self.publicGetCurrencies(params)
|
|
#
|
|
# {
|
|
# "id": "USDT",
|
|
# "name": "Tether",
|
|
# "min_size": "0.000001",
|
|
# "status": "online",
|
|
# "message": "",
|
|
# "max_precision": "0.000001",
|
|
# "convertible_to": [],
|
|
# "details": {
|
|
# "type": "crypto",
|
|
# "symbol": null,
|
|
# "network_confirmations": 14,
|
|
# "sort_order": 0,
|
|
# "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}",
|
|
# "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}",
|
|
# "push_payment_methods": [],
|
|
# "group_types": [],
|
|
# "display_name": null,
|
|
# "processing_time_seconds": null,
|
|
# "min_withdrawal_amount": 0.000001,
|
|
# "max_withdrawal_amount": 20000000
|
|
# },
|
|
# "default_network": "ethereum",
|
|
# "supported_networks": [
|
|
# {
|
|
# "id": "ethereum",
|
|
# "name": "Ethereum",
|
|
# "status": "online",
|
|
# "contract_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
# "crypto_address_link": "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a={{address}}",
|
|
# "crypto_transaction_link": "https://etherscan.io/tx/0x{{txId}}",
|
|
# "min_withdrawal_amount": 0.000001,
|
|
# "max_withdrawal_amount": 20000000,
|
|
# "network_confirmations": 14,
|
|
# "processing_time_seconds": null
|
|
# }
|
|
# ],
|
|
# "display_name": "USDT"
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
for i in range(0, len(response)):
|
|
currency = response[i]
|
|
id = self.safe_string(currency, 'id')
|
|
name = self.safe_string(currency, 'name')
|
|
code = self.safe_currency_code(id)
|
|
details = self.safe_dict(currency, 'details', {})
|
|
networks: dict = {}
|
|
supportedNetworks = self.safe_list(currency, 'supported_networks', [])
|
|
for j in range(0, len(supportedNetworks)):
|
|
network = supportedNetworks[j]
|
|
networkId = self.safe_string(network, 'id')
|
|
networkCode = self.network_id_to_code(networkId)
|
|
networks[networkCode] = {
|
|
'id': networkId,
|
|
'name': self.safe_string(network, 'name'),
|
|
'network': networkCode,
|
|
'active': self.safe_string(network, 'status') == 'online',
|
|
'withdraw': None,
|
|
'deposit': None,
|
|
'fee': None,
|
|
'precision': None,
|
|
'limits': {
|
|
'withdraw': {
|
|
'min': self.safe_number(network, 'min_withdrawal_amount'),
|
|
'max': self.safe_number(network, 'max_withdrawal_amount'),
|
|
},
|
|
},
|
|
'contract': self.safe_string(network, 'contract_address'),
|
|
'info': network,
|
|
}
|
|
result[code] = self.safe_currency_structure({
|
|
'id': id,
|
|
'code': code,
|
|
'info': currency,
|
|
'type': self.safe_string(details, 'type'),
|
|
'name': name,
|
|
'active': self.safe_string(currency, 'status') == 'online',
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': None,
|
|
'precision': self.safe_number(currency, 'max_precision'),
|
|
'limits': {
|
|
'amount': {
|
|
'min': self.safe_number(details, 'min_size'),
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': self.safe_number(details, 'min_withdrawal_amount'),
|
|
'max': self.safe_number(details, 'max_withdrawal_amount'),
|
|
},
|
|
},
|
|
'networks': networks,
|
|
})
|
|
return result
|
|
|
|
async def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for coinbaseexchange
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducts
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = await self.publicGetProducts(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "BTCAUCTION-USD",
|
|
# "base_currency": "BTC",
|
|
# "quote_currency": "USD",
|
|
# "base_min_size": "0.000016",
|
|
# "base_max_size": "1500",
|
|
# "quote_increment": "0.01",
|
|
# "base_increment": "0.00000001",
|
|
# "display_name": "BTCAUCTION/USD",
|
|
# "min_market_funds": "1",
|
|
# "max_market_funds": "20000000",
|
|
# "margin_enabled": False,
|
|
# "fx_stablecoin": False,
|
|
# "max_slippage_percentage": "0.02000000",
|
|
# "post_only": False,
|
|
# "limit_only": False,
|
|
# "cancel_only": True,
|
|
# "trading_disabled": False,
|
|
# "status": "online",
|
|
# "status_message": '',
|
|
# "auction_mode": False
|
|
# },
|
|
# {
|
|
# "id": "BTC-USD",
|
|
# "base_currency": "BTC",
|
|
# "quote_currency": "USD",
|
|
# "base_min_size": "0.000016",
|
|
# "base_max_size": "1500",
|
|
# "quote_increment": "0.01",
|
|
# "base_increment": "0.00000001",
|
|
# "display_name": "BTC/USD",
|
|
# "min_market_funds": "1",
|
|
# "max_market_funds": "20000000",
|
|
# "margin_enabled": False,
|
|
# "fx_stablecoin": False,
|
|
# "max_slippage_percentage": "0.02000000",
|
|
# "post_only": False,
|
|
# "limit_only": False,
|
|
# "cancel_only": False,
|
|
# "trading_disabled": False,
|
|
# "status": "online",
|
|
# "status_message": '',
|
|
# "auction_mode": False
|
|
# }
|
|
# ]
|
|
#
|
|
result = []
|
|
for i in range(0, len(response)):
|
|
market = response[i]
|
|
id = self.safe_string(market, 'id')
|
|
baseId, quoteId = id.split('-')
|
|
# BTCAUCTION-USD vs BTC-USD conflict workaround, see the output sample above
|
|
# baseId = self.safe_string(market, 'base_currency')
|
|
# quoteId = self.safe_string(market, 'quote_currency')
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
status = self.safe_string(market, 'status')
|
|
result.append(self.extend(self.fees['trading'], {
|
|
'id': id,
|
|
'symbol': base + '/' + quote,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': None,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': None,
|
|
'type': 'spot',
|
|
'spot': True,
|
|
'margin': self.safe_value(market, 'margin_enabled'),
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'active': (status == 'online'),
|
|
'contract': False,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'contractSize': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.safe_number(market, 'base_increment'),
|
|
'price': self.safe_number(market, 'quote_increment'),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'min_market_funds'),
|
|
'max': None,
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': market,
|
|
}))
|
|
return result
|
|
|
|
async def fetch_accounts(self, params={}) -> List[Account]:
|
|
"""
|
|
fetch all the accounts associated with a profile
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `account structures <https://docs.ccxt.com/#/?id=account-structure>` indexed by the account type
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.privateGetAccounts(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0",
|
|
# "currency": "BTC",
|
|
# "balance": "0.0000000000000000",
|
|
# "available": "0",
|
|
# "hold": "0.0000000000000000",
|
|
# "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
|
|
# },
|
|
# {
|
|
# "id": "f75fa69a-1ad1-4a80-bd61-ee7faa6135a3",
|
|
# "currency": "USDC",
|
|
# "balance": "0.0000000000000000",
|
|
# "available": "0",
|
|
# "hold": "0.0000000000000000",
|
|
# "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
|
|
# },
|
|
# ]
|
|
#
|
|
return self.parse_accounts(response, params)
|
|
|
|
def parse_account(self, account):
|
|
#
|
|
# {
|
|
# "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0",
|
|
# "currency": "BTC",
|
|
# "balance": "0.0000000000000000",
|
|
# "available": "0",
|
|
# "hold": "0.0000000000000000",
|
|
# "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(account, 'currency')
|
|
return {
|
|
'id': self.safe_string(account, 'id'),
|
|
'type': None,
|
|
'code': self.safe_currency_code(currencyId),
|
|
'info': account,
|
|
}
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
result: dict = {'info': response}
|
|
for i in range(0, len(response)):
|
|
balance = response[i]
|
|
currencyId = self.safe_string(balance, 'currency')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['free'] = self.safe_string(balance, 'available')
|
|
account['used'] = self.safe_string(balance, 'hold')
|
|
account['total'] = self.safe_string(balance, 'balance')
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
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.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts
|
|
|
|
: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.privateGetAccounts(params)
|
|
return self.parse_balance(response)
|
|
|
|
async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductbook
|
|
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
# level 1 - only the best bid and ask
|
|
# level 2 - top 50 bids and asks(aggregated)
|
|
# level 3 - full order book(non aggregated)
|
|
request: dict = {
|
|
'id': self.market_id(symbol),
|
|
'level': 2, # 1 best bidask, 2 aggregated, 3 full
|
|
}
|
|
response = await self.publicGetProductsIdBook(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "sequence":1924393896,
|
|
# "bids":[
|
|
# ["0.01825","24.34811287",2],
|
|
# ["0.01824","72.5463",3],
|
|
# ["0.01823","424.54298049",6],
|
|
# ],
|
|
# "asks":[
|
|
# ["0.01826","171.10414904",4],
|
|
# ["0.01827","22.60427028",1],
|
|
# ["0.01828","397.46018784",7],
|
|
# ]
|
|
# }
|
|
#
|
|
orderbook = self.parse_order_book(response, symbol)
|
|
orderbook['nonce'] = self.safe_integer(response, 'sequence')
|
|
return orderbook
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# fetchTickers
|
|
#
|
|
# [
|
|
# 1639472400, # timestamp
|
|
# 4.26, # low
|
|
# 4.38, # high
|
|
# 4.35, # open
|
|
# 4.27 # close
|
|
# ]
|
|
#
|
|
# fetchTicker
|
|
#
|
|
# publicGetProductsIdTicker
|
|
#
|
|
# {
|
|
# "trade_id":843439,
|
|
# "price":"0.997999",
|
|
# "size":"80.29769",
|
|
# "time":"2020-01-28T02:13:33.012523Z",
|
|
# "bid":"0.997094",
|
|
# "ask":"0.998",
|
|
# "volume":"1903188.03750000"
|
|
# }
|
|
#
|
|
# publicGetProductsIdStats
|
|
#
|
|
# {
|
|
# "open": "34.19000000",
|
|
# "high": "95.70000000",
|
|
# "low": "7.06000000",
|
|
# "volume": "2.41000000"
|
|
# }
|
|
#
|
|
timestamp = None
|
|
bid = None
|
|
ask = None
|
|
last = None
|
|
high = None
|
|
low = None
|
|
open = None
|
|
volume = None
|
|
symbol = None if (market is None) else market['symbol']
|
|
if isinstance(ticker, list):
|
|
last = self.safe_string(ticker, 4)
|
|
timestamp = self.milliseconds()
|
|
else:
|
|
timestamp = self.parse8601(self.safe_value(ticker, 'time'))
|
|
bid = self.safe_string(ticker, 'bid')
|
|
ask = self.safe_string(ticker, 'ask')
|
|
high = self.safe_string(ticker, 'high')
|
|
low = self.safe_string(ticker, 'low')
|
|
open = self.safe_string(ticker, 'open')
|
|
last = self.safe_string_2(ticker, 'price', 'last')
|
|
volume = self.safe_string(ticker, 'volume')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': high,
|
|
'low': low,
|
|
'bid': bid,
|
|
'bidVolume': None,
|
|
'ask': ask,
|
|
'askVolume': None,
|
|
'vwap': None,
|
|
'open': open,
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': volume,
|
|
'quoteVolume': None,
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
async 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://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproduct
|
|
|
|
: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)
|
|
request: dict = {}
|
|
response = await self.publicGetProductsSparkLines(self.extend(request, params))
|
|
#
|
|
# {
|
|
# YYY-USD: [
|
|
# [
|
|
# 1639472400, # timestamp
|
|
# 4.26, # low
|
|
# 4.38, # high
|
|
# 4.35, # open
|
|
# 4.27 # close
|
|
# ],
|
|
# [
|
|
# 1639468800,
|
|
# 4.31,
|
|
# 4.45,
|
|
# 4.35,
|
|
# 4.35
|
|
# ],
|
|
# ]
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
marketIds = list(response.keys())
|
|
delimiter = '-'
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
entry = self.safe_value(response, marketId, [])
|
|
first = self.safe_value(entry, 0, [])
|
|
market = self.safe_market(marketId, None, delimiter)
|
|
symbol = market['symbol']
|
|
result[symbol] = self.parse_ticker(first, market)
|
|
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
|
|
|
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductticker
|
|
|
|
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
: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 = {
|
|
'id': market['id'],
|
|
}
|
|
# publicGetProductsIdTicker or publicGetProductsIdStats
|
|
method = self.safe_string(self.options, 'fetchTickerMethod', 'publicGetProductsIdTicker')
|
|
response = await getattr(self, method)(self.extend(request, params))
|
|
#
|
|
# publicGetProductsIdTicker
|
|
#
|
|
# {
|
|
# "trade_id":843439,
|
|
# "price":"0.997999",
|
|
# "size":"80.29769",
|
|
# "time":"2020-01-28T02:13:33.012523Z",
|
|
# "bid":"0.997094",
|
|
# "ask":"0.998",
|
|
# "volume":"1903188.03750000"
|
|
# }
|
|
#
|
|
# publicGetProductsIdStats
|
|
#
|
|
# {
|
|
# "open": "34.19000000",
|
|
# "high": "95.70000000",
|
|
# "low": "7.06000000",
|
|
# "volume": "2.41000000"
|
|
# }
|
|
#
|
|
return self.parse_ticker(response, market)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# {
|
|
# "type": "match",
|
|
# "trade_id": 82047307,
|
|
# "maker_order_id": "0f358725-2134-435e-be11-753912a326e0",
|
|
# "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf",
|
|
# "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
|
|
# "side": "sell",
|
|
# "size": "0.00513192",
|
|
# "price": "9314.78",
|
|
# "product_id": "BTC-USD",
|
|
# "profile_id": "6244401d-c078-40d9-b305-7ad3551bc3b0",
|
|
# "sequence": 12038915443,
|
|
# "time": "2020-01-31T20:03:41.158814Z"
|
|
# "created_at": "2014-11-07T22:19:28.578544Z",
|
|
# "liquidity": "T",
|
|
# "fee": "0.00025",
|
|
# "settled": True,
|
|
# "usd_volume": "0.0924556000000000",
|
|
# "user_id": "595eb864313c2b02ddf2937d"
|
|
# }
|
|
#
|
|
timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'created_at'))
|
|
marketId = self.safe_string(trade, 'product_id')
|
|
market = self.safe_market(marketId, market, '-')
|
|
feeRate = None
|
|
takerOrMaker = None
|
|
cost = None
|
|
feeCurrencyId = self.safe_string_lower(market, 'quoteId')
|
|
if feeCurrencyId is not None:
|
|
costField = feeCurrencyId + '_value'
|
|
cost = self.safe_string(trade, costField)
|
|
liquidity = self.safe_string(trade, 'liquidity')
|
|
if liquidity is not None:
|
|
takerOrMaker = 'taker' if (liquidity == 'T') else 'maker'
|
|
feeRate = self.safe_string(market, takerOrMaker)
|
|
feeCost = self.safe_string_2(trade, 'fill_fees', 'fee')
|
|
fee = {
|
|
'cost': feeCost,
|
|
'currency': market['quote'],
|
|
'rate': feeRate,
|
|
}
|
|
id = self.safe_string(trade, 'trade_id')
|
|
side = 'sell' if (trade['side'] == 'buy') else 'buy'
|
|
orderId = self.safe_string(trade, 'order_id')
|
|
# Coinbase Pro returns inverted side to fetchMyTrades vs fetchTrades
|
|
makerOrderId = self.safe_string(trade, 'maker_order_id')
|
|
takerOrderId = self.safe_string(trade, 'taker_order_id')
|
|
if (orderId is not None) or ((makerOrderId is not None) and (takerOrderId is not None)):
|
|
side = 'buy' if (trade['side'] == 'buy') else 'sell'
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'size')
|
|
return self.safe_trade({
|
|
'id': id,
|
|
'order': orderId,
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'type': None,
|
|
'takerOrMaker': takerOrMaker,
|
|
'side': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'fee': fee,
|
|
'cost': cost,
|
|
}, market)
|
|
|
|
async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfills
|
|
|
|
fetch all trades made by the user
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: the latest time in ms to fetch trades for
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
|
if paginate:
|
|
return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100)
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'product_id': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
if since is not None:
|
|
request['start_date'] = self.iso8601(since)
|
|
until = self.safe_value_2(params, 'until', 'end_date')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['end_date'] = self.iso8601(until)
|
|
response = await self.privateGetFills(self.extend(request, params))
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducttrades
|
|
|
|
get the list of most recent trades for a particular symbol
|
|
: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 = {
|
|
'id': market['id'], # fixes issue #2
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit # default 100
|
|
response = await self.publicGetProductsIdTrades(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "trade_id": "15035219",
|
|
# "side": "sell",
|
|
# "size": "0.27426731",
|
|
# "price": "25820.42000000",
|
|
# "time": "2023-09-10T13:47:41.447577Z"
|
|
# },
|
|
# ]
|
|
#
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
async def fetch_trading_fees(self, params={}) -> TradingFees:
|
|
"""
|
|
fetch the trading fees for multiple markets
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfees
|
|
|
|
: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.privateGetFees(params)
|
|
#
|
|
# {
|
|
# "maker_fee_rate": "0.0050",
|
|
# "taker_fee_rate": "0.0050",
|
|
# "usd_volume": "43806.92"
|
|
# }
|
|
#
|
|
maker = self.safe_number(response, 'maker_fee_rate')
|
|
taker = self.safe_number(response, 'taker_fee_rate')
|
|
result: dict = {}
|
|
for i in range(0, len(self.symbols)):
|
|
symbol = self.symbols[i]
|
|
result[symbol] = {
|
|
'info': response,
|
|
'symbol': symbol,
|
|
'maker': maker,
|
|
'taker': taker,
|
|
'percentage': True,
|
|
'tierBased': True,
|
|
}
|
|
return result
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# [
|
|
# 1591514160,
|
|
# 0.02507,
|
|
# 0.02507,
|
|
# 0.02507,
|
|
# 0.02507,
|
|
# 0.02816506
|
|
# ]
|
|
#
|
|
return [
|
|
self.safe_timestamp(ohlcv, 0),
|
|
self.safe_number(ohlcv, 3),
|
|
self.safe_number(ohlcv, 2),
|
|
self.safe_number(ohlcv, 1),
|
|
self.safe_number(ohlcv, 4),
|
|
self.safe_number(ohlcv, 5),
|
|
]
|
|
|
|
async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductcandles
|
|
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
: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]: the latest time in ms to fetch 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 int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False)
|
|
if paginate:
|
|
return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300)
|
|
market = self.market(symbol)
|
|
parsedTimeframe = self.safe_integer(self.timeframes, timeframe)
|
|
request: dict = {
|
|
'id': market['id'],
|
|
}
|
|
if parsedTimeframe is not None:
|
|
request['granularity'] = parsedTimeframe
|
|
else:
|
|
request['granularity'] = timeframe
|
|
until = self.safe_value_2(params, 'until', 'end')
|
|
params = self.omit(params, ['until'])
|
|
if since is not None:
|
|
request['start'] = self.iso8601(since)
|
|
if limit is None:
|
|
# https://docs.pro.coinbase.com/#get-historic-rates
|
|
limit = 300 # max = 300
|
|
else:
|
|
limit = min(300, limit)
|
|
if until is None:
|
|
parsedTimeframeMilliseconds = parsedTimeframe * 1000
|
|
if self.is_round_number(since % parsedTimeframeMilliseconds):
|
|
request['end'] = self.iso8601(self.sum((limit - 1) * parsedTimeframeMilliseconds, since))
|
|
else:
|
|
request['end'] = self.iso8601(self.sum(limit * parsedTimeframeMilliseconds, since))
|
|
else:
|
|
request['end'] = self.iso8601(until)
|
|
response = await self.publicGetProductsIdCandles(self.extend(request, params))
|
|
#
|
|
# [
|
|
# [1591514160,0.02507,0.02507,0.02507,0.02507,0.02816506],
|
|
# [1591514100,0.02507,0.02507,0.02507,0.02507,1.63830323],
|
|
# [1591514040,0.02505,0.02507,0.02505,0.02507,0.19918178]
|
|
# ]
|
|
#
|
|
return self.parse_ohlcvs(response, market, timeframe, since, limit)
|
|
|
|
async def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
response = await self.publicGetTime(params)
|
|
#
|
|
# {
|
|
# "iso":"2020-05-12T08:00:51.504Z",
|
|
# "epoch":1589270451.504
|
|
# }
|
|
#
|
|
return self.safe_timestamp(response, 'epoch')
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'pending': 'open',
|
|
'active': 'open',
|
|
'open': 'open',
|
|
'done': 'closed',
|
|
'canceled': 'canceled',
|
|
'canceling': 'open',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# createOrder
|
|
#
|
|
# {
|
|
# "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
|
|
# "price": "0.10000000",
|
|
# "size": "0.01000000",
|
|
# "product_id": "BTC-USD",
|
|
# "side": "buy",
|
|
# "stp": "dc",
|
|
# "type": "limit",
|
|
# "time_in_force": "GTC",
|
|
# "post_only": False,
|
|
# "created_at": "2016-12-08T20:02:28.53864Z",
|
|
# "fill_fees": "0.0000000000000000",
|
|
# "filled_size": "0.00000000",
|
|
# "executed_value": "0.0000000000000000",
|
|
# "status": "pending",
|
|
# "settled": False
|
|
# }
|
|
#
|
|
timestamp = self.parse8601(self.safe_string(order, 'created_at'))
|
|
marketId = self.safe_string(order, 'product_id')
|
|
market = self.safe_market(marketId, market, '-')
|
|
status = self.parse_order_status(self.safe_string(order, 'status'))
|
|
doneReason = self.safe_string(order, 'done_reason')
|
|
if (status == 'closed') and (doneReason == 'canceled'):
|
|
status = 'canceled'
|
|
price = self.safe_string(order, 'price')
|
|
filled = self.safe_string(order, 'filled_size')
|
|
amount = self.safe_string(order, 'size', filled)
|
|
cost = self.safe_string(order, 'executed_value')
|
|
feeCost = self.safe_number(order, 'fill_fees')
|
|
fee = None
|
|
if feeCost is not None:
|
|
fee = {
|
|
'cost': feeCost,
|
|
'currency': market['quote'],
|
|
'rate': None,
|
|
}
|
|
id = self.safe_string(order, 'id')
|
|
type = self.safe_string(order, 'type')
|
|
side = self.safe_string(order, 'side')
|
|
timeInForce = self.safe_string(order, 'time_in_force')
|
|
postOnly = self.safe_value(order, 'post_only')
|
|
triggerPrice = self.safe_number(order, 'stop_price')
|
|
clientOrderId = self.safe_string(order, 'client_oid')
|
|
return self.safe_order({
|
|
'id': id,
|
|
'clientOrderId': clientOrderId,
|
|
'info': order,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'status': status,
|
|
'symbol': market['symbol'],
|
|
'type': type,
|
|
'timeInForce': timeInForce,
|
|
'postOnly': postOnly,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': triggerPrice,
|
|
'cost': cost,
|
|
'amount': amount,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'fee': fee,
|
|
'average': None,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
async def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorder
|
|
|
|
fetches information on an order made by the user
|
|
:param str id: the order id
|
|
:param str symbol: not used by coinbaseexchange fetchOrder
|
|
: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 = {}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
|
|
method = None
|
|
if clientOrderId is None:
|
|
method = 'privateGetOrdersId'
|
|
request['id'] = id
|
|
else:
|
|
method = 'privateGetOrdersClientClientOid'
|
|
request['client_oid'] = clientOrderId
|
|
params = self.omit(params, ['clientOrderId', 'client_oid'])
|
|
response = await getattr(self, method)(self.extend(request, params))
|
|
return self.parse_order(response)
|
|
|
|
async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all the trades made from a single order
|
|
:param str id: order id
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'order_id': id,
|
|
}
|
|
response = await self.privateGetFills(self.extend(request, params))
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
|
|
|
|
fetches information on multiple orders made by the user
|
|
: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 int [params.until]: the latest time in ms to fetch open orders for
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'all',
|
|
}
|
|
return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
|
|
|
|
fetch all unfilled currently open 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 int [params.until]: the latest time in ms to fetch open orders 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 Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate')
|
|
if paginate:
|
|
return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params, 100)
|
|
request: dict = {}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['product_id'] = market['id']
|
|
if limit is not None:
|
|
request['limit'] = limit # default 100
|
|
if since is not None:
|
|
request['start_date'] = self.iso8601(since)
|
|
until = self.safe_value_2(params, 'until', 'end_date')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['end_date'] = self.iso8601(until)
|
|
response = await self.privateGetOrders(self.extend(request, params))
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
|
|
|
|
fetches information on multiple closed orders made by the user
|
|
: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 int [params.until]: the latest time in ms to fetch open orders for
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'done',
|
|
}
|
|
return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postorders
|
|
|
|
create a trade order
|
|
: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 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
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
# common params --------------------------------------------------
|
|
# 'client_oid': clientOrderId,
|
|
'type': type,
|
|
'side': side,
|
|
'product_id': market['id'],
|
|
# 'size': self.amount_to_precision(symbol, amount),
|
|
# 'stp': 'dc', # self-trade prevention, dc = decrease and cancel, co = cancel oldest, cn = cancel newest, cb = cancel both
|
|
# 'stop': 'loss', # "loss" = stop loss below price, "entry" = take profit above price
|
|
# 'stop_price': self.price_to_precision(symbol, price),
|
|
# limit order params ---------------------------------------------
|
|
# 'price': self.price_to_precision(symbol, price),
|
|
# 'size': self.amount_to_precision(symbol, amount),
|
|
# 'time_in_force': 'GTC', # GTC, GTT, IOC, or FOK
|
|
# 'cancel_after' [optional]* min, hour, day, requires time_in_force to be GTT
|
|
# 'post_only': False, # invalid when time_in_force is IOC or FOK
|
|
# market order params --------------------------------------------
|
|
# 'size': self.amount_to_precision(symbol, amount),
|
|
# 'funds': self.cost_to_precision(symbol, amount),
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
|
|
if clientOrderId is not None:
|
|
request['client_oid'] = clientOrderId
|
|
triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice'])
|
|
if triggerPrice is not None:
|
|
request['stop_price'] = self.price_to_precision(symbol, triggerPrice)
|
|
timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force')
|
|
if timeInForce is not None:
|
|
request['time_in_force'] = timeInForce
|
|
postOnly = self.safe_value_2(params, 'postOnly', 'post_only', False)
|
|
if postOnly:
|
|
request['post_only'] = True
|
|
params = self.omit(params, ['timeInForce', 'time_in_force', 'stopPrice', 'stop_price', 'clientOrderId', 'client_oid', 'postOnly', 'post_only', 'triggerPrice'])
|
|
if type == 'limit':
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
request['size'] = self.amount_to_precision(symbol, amount)
|
|
elif type == 'market':
|
|
cost = self.safe_number_2(params, 'cost', 'funds')
|
|
if cost is None:
|
|
if price is not None:
|
|
cost = amount * price
|
|
else:
|
|
params = self.omit(params, ['cost', 'funds'])
|
|
if cost is not None:
|
|
request['funds'] = self.cost_to_precision(symbol, cost)
|
|
else:
|
|
request['size'] = self.amount_to_precision(symbol, amount)
|
|
response = await self.privatePostOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
|
|
# "price": "0.10000000",
|
|
# "size": "0.01000000",
|
|
# "product_id": "BTC-USD",
|
|
# "side": "buy",
|
|
# "stp": "dc",
|
|
# "type": "limit",
|
|
# "time_in_force": "GTC",
|
|
# "post_only": False,
|
|
# "created_at": "2016-12-08T20:02:28.53864Z",
|
|
# "fill_fees": "0.0000000000000000",
|
|
# "filled_size": "0.00000000",
|
|
# "executed_value": "0.0000000000000000",
|
|
# "status": "pending",
|
|
# "settled": False
|
|
# }
|
|
#
|
|
return self.parse_order(response, market)
|
|
|
|
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorder
|
|
|
|
cancels an open 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
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
# 'product_id': market['id'], # the request will be more performant if you include it
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
|
|
method = None
|
|
if clientOrderId is None:
|
|
method = 'privateDeleteOrdersId'
|
|
request['id'] = id
|
|
else:
|
|
method = 'privateDeleteOrdersClientClientOid'
|
|
request['client_oid'] = clientOrderId
|
|
params = self.omit(params, ['clientOrderId', 'client_oid'])
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['product_id'] = market['symbol'] # the request will be more performant if you include it
|
|
response = await getattr(self, method)(self.extend(request, params))
|
|
return self.safe_order({'info': response})
|
|
|
|
async def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorders
|
|
|
|
cancel all open orders
|
|
:param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
|
|
: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 = {}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['product_id'] = market['symbol'] # the request will be more performant if you include it
|
|
response = await self.privateDeleteOrders(self.extend(request, params))
|
|
return [self.safe_order({'info': response})]
|
|
|
|
async def fetch_payment_methods(self, params={}):
|
|
return await self.privateGetPaymentMethods(params)
|
|
|
|
async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawpaymentmethod
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawcoinbaseaccount
|
|
|
|
: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)
|
|
self.check_address(address)
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'currency': currency['id'],
|
|
'amount': amount,
|
|
}
|
|
method = 'privatePostWithdrawals'
|
|
if 'payment_method_id' in params:
|
|
method += 'PaymentMethod'
|
|
elif 'coinbase_account_id' in params:
|
|
method += 'CoinbaseAccount'
|
|
else:
|
|
method += 'Crypto'
|
|
request['crypto_address'] = address
|
|
if tag is not None:
|
|
request['destination_tag'] = tag
|
|
response = await getattr(self, method)(self.extend(request, params))
|
|
if not response:
|
|
raise ExchangeError(self.id + ' withdraw() error: ' + self.json(response))
|
|
return self.parse_transaction(response, currency)
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
types: dict = {
|
|
'transfer': 'transfer', # Funds moved between portfolios
|
|
'match': 'trade', # Funds moved result of a trade
|
|
'fee': 'fee', # Fee result of a trade
|
|
'rebate': 'rebate', # Fee rebate
|
|
'conversion': 'trade', # Funds converted between fiat currency and a stablecoin
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
|
# {
|
|
# "id": "12087495079",
|
|
# "amount": "-0.0100000000000000",
|
|
# "balance": "0.0645419900000000",
|
|
# "created_at": "2021-10-28T17:14:32.593168Z",
|
|
# "type": "transfer",
|
|
# "details": {
|
|
# "from": "2f74edf7-1440-4586-86dc-ae58c5693691",
|
|
# "profile_transfer_id": "3ef093ad-2482-40d1-8ede-2f89cff5099e",
|
|
# "to": "dda99503-4980-4b60-9549-0b770ee51336"
|
|
# }
|
|
# },
|
|
# {
|
|
# "id": "11740725774",
|
|
# "amount": "-1.7565669701255000",
|
|
# "balance": "0.0016490047745000",
|
|
# "created_at": "2021-10-22T03:47:34.764122Z",
|
|
# "type": "fee",
|
|
# "details": {
|
|
# "order_id": "ad06abf4-95ab-432a-a1d8-059ef572e296",
|
|
# "product_id": "ETH-DAI",
|
|
# "trade_id": "1740617"
|
|
# }
|
|
# }
|
|
id = self.safe_string(item, 'id')
|
|
amountString = self.safe_string(item, 'amount')
|
|
direction = None
|
|
afterString = self.safe_string(item, 'balance')
|
|
beforeString = Precise.string_sub(afterString, amountString)
|
|
if Precise.string_lt(amountString, '0'):
|
|
direction = 'out'
|
|
amountString = Precise.string_abs(amountString)
|
|
else:
|
|
direction = 'in'
|
|
amount = self.parse_number(amountString)
|
|
after = self.parse_number(afterString)
|
|
before = self.parse_number(beforeString)
|
|
timestamp = self.parse8601(self.safe_value(item, 'created_at'))
|
|
type = self.parse_ledger_entry_type(self.safe_string(item, 'type'))
|
|
code = self.safe_currency_code(None, currency)
|
|
details = self.safe_value(item, 'details', {})
|
|
account = None
|
|
referenceAccount = None
|
|
referenceId = None
|
|
if type == 'transfer':
|
|
account = self.safe_string(details, 'from')
|
|
referenceAccount = self.safe_string(details, 'to')
|
|
referenceId = self.safe_string(details, 'profile_transfer_id')
|
|
else:
|
|
referenceId = self.safe_string(details, 'order_id')
|
|
status = 'ok'
|
|
return self.safe_ledger_entry({
|
|
'info': item,
|
|
'id': id,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'direction': direction,
|
|
'account': account,
|
|
'referenceAccount': referenceAccount,
|
|
'referenceId': referenceId,
|
|
'type': type,
|
|
'currency': code,
|
|
'amount': amount,
|
|
'before': before,
|
|
'after': after,
|
|
'status': status,
|
|
'fee': None,
|
|
}, currency)
|
|
|
|
async 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://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger
|
|
|
|
: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 int [params.until]: the latest time in ms to fetch trades for
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
|
|
"""
|
|
# https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger
|
|
if code is None:
|
|
raise ArgumentsRequired(self.id + ' fetchLedger() requires a code param')
|
|
await self.load_markets()
|
|
await self.load_accounts()
|
|
currency = self.currency(code)
|
|
accountsByCurrencyCode = self.index_by(self.accounts, 'code')
|
|
account = self.safe_value(accountsByCurrencyCode, code)
|
|
if account is None:
|
|
raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code)
|
|
request: dict = {
|
|
'id': account['id'],
|
|
# 'start_date': self.iso8601(since),
|
|
# 'end_date': self.iso8601(self.milliseconds()),
|
|
# 'before': 'cursor', # sets start cursor to before date
|
|
# 'after': 'cursor', # sets end cursor to after date
|
|
# 'limit': limit, # default 100
|
|
# 'profile_id': 'string'
|
|
}
|
|
if since is not None:
|
|
request['start_date'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['limit'] = limit # default 100
|
|
until = self.safe_value_2(params, 'until', 'end_date')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['end_date'] = self.iso8601(until)
|
|
response = await self.privateGetAccountsIdLedger(self.extend(request, params))
|
|
for i in range(0, len(response)):
|
|
response[i]['currency'] = code
|
|
return self.parse_ledger(response, currency, since, limit)
|
|
|
|
async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch history of deposits and withdrawals
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
|
|
|
|
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
|
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
|
:param int [limit]: max number of deposit/withdrawals to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.id]: account id, when defined, the endpoint used is '/accounts/{account_id}/transfers/' instead of '/transfers/'
|
|
:returns dict: a list of `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.load_accounts()
|
|
currency = None
|
|
id = self.safe_string(params, 'id') # account id
|
|
if id is None:
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
accountsByCurrencyCode = self.index_by(self.accounts, 'code')
|
|
account = self.safe_value(accountsByCurrencyCode, code)
|
|
if account is None:
|
|
raise ExchangeError(self.id + ' fetchDepositsWithdrawals() could not find account id for ' + code)
|
|
id = account['id']
|
|
request: dict = {}
|
|
if id is not None:
|
|
request['id'] = id
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
response = None
|
|
if id is None:
|
|
response = await self.privateGetTransfers(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
|
|
# "type": "deposit",
|
|
# "created_at": "2022-12-21 00:48:45.477503+00",
|
|
# "completed_at": null,
|
|
# "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak",
|
|
# "user_id": "6382048209f92as392039dlks2",
|
|
# "amount": "0.01000000",
|
|
# "details": {
|
|
# "network": "litecoin",
|
|
# "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
|
|
# "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
|
|
# "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only
|
|
# "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
|
|
# "tx_service_transaction_id": "1873249104",
|
|
# "coinbase_payment_method_id": ""
|
|
# },
|
|
# "canceled_at": null,
|
|
# "processed_at": null,
|
|
# "user_nonce": null,
|
|
# "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
|
|
# "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
|
|
# "currency": "LTC"
|
|
# }
|
|
# ]
|
|
#
|
|
for i in range(0, len(response)):
|
|
account_id = self.safe_string(response[i], 'account_id')
|
|
account = self.safe_value(self.accountsById, account_id)
|
|
codeInner = self.safe_string(account, 'code')
|
|
response[i]['currency'] = codeInner
|
|
else:
|
|
response = await self.privateGetAccountsIdTransfers(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
|
|
# "type": "deposit",
|
|
# "created_at": "2022-12-21 00:48:45.477503+00",
|
|
# "completed_at": null,
|
|
# "amount": "0.01000000",
|
|
# "details": {
|
|
# "network": "litecoin",
|
|
# "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
|
|
# "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
|
|
# "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only
|
|
# "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
|
|
# "tx_service_transaction_id": "1873249104",
|
|
# "coinbase_payment_method_id": ""
|
|
# },
|
|
# "canceled_at": null,
|
|
# "processed_at": null,
|
|
# "user_nonce": null,
|
|
# "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
|
|
# "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
|
|
# "currency": "LTC"
|
|
# }
|
|
# ]
|
|
#
|
|
for i in range(0, len(response)):
|
|
response[i]['currency'] = code
|
|
return self.parse_transactions(response, currency, since, limit)
|
|
|
|
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.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
|
|
|
|
: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
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'deposit'}, params))
|
|
|
|
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.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
|
|
|
|
: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
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'withdraw'}, params))
|
|
|
|
def parse_transaction_status(self, transaction):
|
|
canceled = self.safe_value(transaction, 'canceled_at')
|
|
if canceled:
|
|
return 'canceled'
|
|
processed = self.safe_value(transaction, 'processed_at')
|
|
completed = self.safe_value(transaction, 'completed_at')
|
|
if completed:
|
|
return 'ok'
|
|
elif processed and not completed:
|
|
return 'failed'
|
|
else:
|
|
return 'pending'
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# privateGetTransfers
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
|
|
# "type": "deposit",
|
|
# "created_at": "2022-12-21 00:48:45.477503+00",
|
|
# "completed_at": null,
|
|
# "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak", # only from privateGetTransfers
|
|
# "user_id": "6382048209f92as392039dlks2", # only from privateGetTransfers
|
|
# "amount": "0.01000000",
|
|
# "details": {
|
|
# "network": "litecoin",
|
|
# "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
|
|
# "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
|
|
# "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17", # withdrawals only
|
|
# "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
|
|
# "tx_service_transaction_id": "1873249104",
|
|
# "coinbase_payment_method_id": ""
|
|
# },
|
|
# "canceled_at": null,
|
|
# "processed_at": null,
|
|
# "user_nonce": null,
|
|
# "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
|
|
# "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
|
|
# "currency": "LTC"
|
|
# }
|
|
# ]
|
|
#
|
|
details = self.safe_value(transaction, 'details', {})
|
|
timestamp = self.parse8601(self.safe_string(transaction, 'created_at'))
|
|
currencyId = self.safe_string(transaction, 'currency')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
amount = self.safe_number(transaction, 'amount')
|
|
type = self.safe_string(transaction, 'type')
|
|
address = self.safe_string(details, 'crypto_address')
|
|
address = self.safe_string(transaction, 'crypto_address', address)
|
|
fee = {
|
|
'currency': None,
|
|
'cost': None,
|
|
'rate': None,
|
|
}
|
|
if type == 'withdraw':
|
|
type = 'withdrawal'
|
|
address = self.safe_string(details, 'sent_to_address', address)
|
|
feeCost = self.safe_number(details, 'fee')
|
|
if feeCost is not None:
|
|
if amount is not None:
|
|
amount -= feeCost
|
|
fee['cost'] = feeCost
|
|
fee['currency'] = code
|
|
networkId = self.safe_string(details, 'network')
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string(transaction, 'id'),
|
|
'txid': self.safe_string(details, 'crypto_transaction_hash'),
|
|
'type': type,
|
|
'currency': code,
|
|
'network': self.network_id_to_code(networkId),
|
|
'amount': amount,
|
|
'status': self.parse_transaction_status(transaction),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'address': address,
|
|
'addressFrom': None,
|
|
'addressTo': self.safe_string(details, 'crypto_address'),
|
|
'tag': self.safe_string(details, 'destination_tag'),
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': self.parse8601(self.safe_string(transaction, 'processed_at')),
|
|
'comment': None,
|
|
'internal': False,
|
|
'fee': fee,
|
|
}
|
|
|
|
async def create_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
create a currency deposit address
|
|
|
|
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses
|
|
|
|
:param str code: unified currency code of the currency for the deposit address
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
accounts = self.safe_value(self.options, 'coinbaseAccounts')
|
|
if accounts is None:
|
|
accounts = await self.privateGetCoinbaseAccounts()
|
|
self.options['coinbaseAccounts'] = accounts # cache it
|
|
self.options['coinbaseAccountsByCurrencyId'] = self.index_by(accounts, 'currency')
|
|
currencyId = currency['id']
|
|
account = self.safe_value(self.options['coinbaseAccountsByCurrencyId'], currencyId)
|
|
if account is None:
|
|
# eslint-disable-next-line quotes
|
|
raise InvalidAddress(self.id + " createDepositAddress() could not find currency code " + code + " with id = " + currencyId + " in self.options['coinbaseAccountsByCurrencyId']")
|
|
request: dict = {
|
|
'id': account['id'],
|
|
}
|
|
response = await self.privatePostCoinbaseAccountsIdAddresses(self.extend(request, params))
|
|
address = self.safe_string(response, 'address')
|
|
tag = self.safe_string(response, 'destination_tag')
|
|
return {
|
|
'currency': code,
|
|
'address': self.check_address(address),
|
|
'network': None,
|
|
'tag': tag,
|
|
'info': response,
|
|
}
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
request = '/' + self.implode_params(path, params)
|
|
query = self.omit(params, self.extract_params(path))
|
|
if method == 'GET':
|
|
if query:
|
|
request += '?' + self.urlencode(query)
|
|
url = self.implode_hostname(self.urls['api'][api]) + request
|
|
if api == 'private':
|
|
self.check_required_credentials()
|
|
nonce = str(self.nonce())
|
|
payload = ''
|
|
if method != 'GET':
|
|
if query:
|
|
body = self.json(query)
|
|
payload = body
|
|
what = nonce + method + request + payload
|
|
secret = None
|
|
try:
|
|
secret = self.base64_to_binary(self.secret)
|
|
except Exception as e:
|
|
raise AuthenticationError(self.id + ' sign() invalid base64 secret')
|
|
signature = self.hmac(self.encode(what), secret, hashlib.sha256, 'base64')
|
|
headers = {
|
|
'CB-ACCESS-KEY': self.apiKey,
|
|
'CB-ACCESS-SIGN': signature,
|
|
'CB-ACCESS-TIMESTAMP': nonce,
|
|
'CB-ACCESS-PASSPHRASE': self.password,
|
|
'Content-Type': 'application/json',
|
|
}
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if (code == 400) or (code == 404):
|
|
if body[0] == '{':
|
|
message = self.safe_string(response, 'message')
|
|
feedback = self.id + ' ' + message
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
raise ExchangeError(feedback) # unknown message
|
|
raise ExchangeError(self.id + ' ' + body)
|
|
return None
|
|
|
|
async def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}):
|
|
response = await self.fetch2(path, api, method, params, headers, body, config)
|
|
if not isinstance(response, str):
|
|
if 'message' in response:
|
|
raise ExchangeError(self.id + ' ' + self.json(response))
|
|
return response
|