5003 lines
236 KiB
Python
5003 lines
236 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.coinbase import ImplicitAPI
|
||
import asyncio
|
||
import hashlib
|
||
from ccxt.base.types import Account, Any, Balances, Conversion, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction, MarketInterface
|
||
from typing import List
|
||
from ccxt.base.errors import ExchangeError
|
||
from ccxt.base.errors import AuthenticationError
|
||
from ccxt.base.errors import PermissionDenied
|
||
from ccxt.base.errors import ArgumentsRequired
|
||
from ccxt.base.errors import BadRequest
|
||
from ccxt.base.errors import InsufficientFunds
|
||
from ccxt.base.errors import InvalidOrder
|
||
from ccxt.base.errors import OrderNotFound
|
||
from ccxt.base.errors import NotSupported
|
||
from ccxt.base.errors import RateLimitExceeded
|
||
from ccxt.base.errors import InvalidNonce
|
||
from ccxt.base.decimal_to_precision import TICK_SIZE
|
||
from ccxt.base.precise import Precise
|
||
|
||
|
||
class coinbase(Exchange, ImplicitAPI):
|
||
|
||
def describe(self) -> Any:
|
||
return self.deep_extend(super(coinbase, self).describe(), {
|
||
'id': 'coinbase',
|
||
'name': 'Coinbase Advanced',
|
||
'countries': ['US'],
|
||
'pro': True,
|
||
'certified': False,
|
||
# rate-limits:
|
||
# ADVANCED API: https://docs.cloud.coinbase.com/advanced-trade/docs/rest-api-rate-limits
|
||
# - max 30 req/second for private data, 10 req/s for public data
|
||
# DATA API : https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/rate-limiting
|
||
# - max 10000 req/hour(to prevent userland mistakes we apply ~3 req/second RL per call
|
||
'rateLimit': 34,
|
||
'version': 'v2',
|
||
'userAgent': self.userAgents['chrome'],
|
||
'headers': {
|
||
'CB-VERSION': '2018-05-30',
|
||
},
|
||
'has': {
|
||
'CORS': True,
|
||
'spot': True,
|
||
'margin': False,
|
||
'swap': False,
|
||
'future': False,
|
||
'option': False,
|
||
'addMargin': False,
|
||
'borrowCrossMargin': False,
|
||
'borrowIsolatedMargin': False,
|
||
'borrowMargin': False,
|
||
'cancelOrder': True,
|
||
'cancelOrders': True,
|
||
'closeAllPositions': False,
|
||
'closePosition': True,
|
||
'createConvertTrade': True,
|
||
'createDepositAddress': True,
|
||
'createLimitBuyOrder': True,
|
||
'createLimitSellOrder': True,
|
||
'createMarketBuyOrder': True,
|
||
'createMarketBuyOrderWithCost': True,
|
||
'createMarketOrderWithCost': False,
|
||
'createMarketSellOrder': True,
|
||
'createMarketSellOrderWithCost': False,
|
||
'createOrder': True,
|
||
'createOrderWithTakeProfitAndStopLoss': False,
|
||
'createOrderWithTakeProfitAndStopLossWs': False,
|
||
'createPostOnlyOrder': True,
|
||
'createReduceOnlyOrder': False,
|
||
'createStopLimitOrder': True,
|
||
'createStopMarketOrder': False,
|
||
'createStopOrder': True,
|
||
'deposit': True,
|
||
'editOrder': True,
|
||
'fetchAccounts': True,
|
||
'fetchBalance': True,
|
||
'fetchBidsAsks': True,
|
||
'fetchBorrowInterest': False,
|
||
'fetchBorrowRate': False,
|
||
'fetchBorrowRateHistories': False,
|
||
'fetchBorrowRateHistory': False,
|
||
'fetchBorrowRates': False,
|
||
'fetchBorrowRatesPerSymbol': False,
|
||
'fetchCanceledOrders': True,
|
||
'fetchClosedOrders': True,
|
||
'fetchConvertQuote': True,
|
||
'fetchConvertTrade': True,
|
||
'fetchConvertTradeHistory': False,
|
||
'fetchCrossBorrowRate': False,
|
||
'fetchCrossBorrowRates': False,
|
||
'fetchCurrencies': True,
|
||
'fetchDeposit': True,
|
||
'fetchDepositAddress': 'emulated',
|
||
'fetchDepositAddresses': False,
|
||
'fetchDepositAddressesByNetwork': True,
|
||
'fetchDepositMethodId': True,
|
||
'fetchDepositMethodIds': True,
|
||
'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,
|
||
'fetchL2OrderBook': 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,
|
||
'fetchMyBuys': True,
|
||
'fetchMyLiquidations': False,
|
||
'fetchMySells': True,
|
||
'fetchMySettlementHistory': False,
|
||
'fetchMyTrades': True,
|
||
'fetchOHLCV': True,
|
||
'fetchOpenInterest': False,
|
||
'fetchOpenInterestHistory': False,
|
||
'fetchOpenInterests': False,
|
||
'fetchOpenOrders': True,
|
||
'fetchOption': False,
|
||
'fetchOptionChain': False,
|
||
'fetchOrder': True,
|
||
'fetchOrderBook': True,
|
||
'fetchOrders': True,
|
||
'fetchPosition': True,
|
||
'fetchPositionHistory': False,
|
||
'fetchPositionMode': False,
|
||
'fetchPositions': True,
|
||
'fetchPositionsForSymbol': False,
|
||
'fetchPositionsHistory': False,
|
||
'fetchPositionsRisk': False,
|
||
'fetchPremiumIndexOHLCV': False,
|
||
'fetchSettlementHistory': False,
|
||
'fetchTicker': True,
|
||
'fetchTickers': True,
|
||
'fetchTime': True,
|
||
'fetchTrades': True,
|
||
'fetchTradingFee': 'emulated',
|
||
'fetchTradingFees': True,
|
||
'fetchVolatilityHistory': False,
|
||
'fetchWithdrawals': True,
|
||
'reduceMargin': False,
|
||
'repayCrossMargin': False,
|
||
'repayIsolatedMargin': False,
|
||
'repayMargin': False,
|
||
'setLeverage': False,
|
||
'setMargin': False,
|
||
'setMarginMode': False,
|
||
'setPositionMode': False,
|
||
'withdraw': True,
|
||
},
|
||
'urls': {
|
||
'logo': 'https://user-images.githubusercontent.com/1294454/40811661-b6eceae2-653a-11e8-829e-10bfadb078cf.jpg',
|
||
'api': {
|
||
'rest': 'https://api.coinbase.com',
|
||
},
|
||
'www': 'https://www.coinbase.com',
|
||
'doc': [
|
||
'https://developers.coinbase.com/api/v2',
|
||
'https://docs.cloud.coinbase.com/advanced-trade/docs/welcome',
|
||
],
|
||
'fees': [
|
||
'https://support.coinbase.com/customer/portal/articles/2109597-buy-sell-bank-transfer-fees',
|
||
'https://www.coinbase.com/advanced-fees',
|
||
],
|
||
'referral': 'https://www.coinbase.com/join/58cbe25a355148797479dbd2',
|
||
},
|
||
'requiredCredentials': {
|
||
'apiKey': True,
|
||
'secret': True,
|
||
},
|
||
'api': {
|
||
'v2': {
|
||
'public': {
|
||
'get': {
|
||
'currencies': 10.6,
|
||
'currencies/crypto': 10.6,
|
||
'time': 10.6,
|
||
'exchange-rates': 10.6,
|
||
'users/{user_id}': 10.6,
|
||
'prices/{symbol}/buy': 10.6,
|
||
'prices/{symbol}/sell': 10.6,
|
||
'prices/{symbol}/spot': 10.6,
|
||
},
|
||
},
|
||
'private': {
|
||
'get': {
|
||
'accounts': 10.6,
|
||
'accounts/{account_id}': 10.6,
|
||
'accounts/{account_id}/addresses': 10.6,
|
||
'accounts/{account_id}/addresses/{address_id}': 10.6,
|
||
'accounts/{account_id}/addresses/{address_id}/transactions': 10.6,
|
||
'accounts/{account_id}/transactions': 10.6,
|
||
'accounts/{account_id}/transactions/{transaction_id}': 10.6,
|
||
'accounts/{account_id}/buys': 10.6,
|
||
'accounts/{account_id}/buys/{buy_id}': 10.6,
|
||
'accounts/{account_id}/sells': 10.6,
|
||
'accounts/{account_id}/sells/{sell_id}': 10.6,
|
||
'accounts/{account_id}/deposits': 10.6,
|
||
'accounts/{account_id}/deposits/{deposit_id}': 10.6,
|
||
'accounts/{account_id}/withdrawals': 10.6,
|
||
'accounts/{account_id}/withdrawals/{withdrawal_id}': 10.6,
|
||
'payment-methods': 10.6,
|
||
'payment-methods/{payment_method_id}': 10.6,
|
||
'user': 10.6,
|
||
'user/auth': 10.6,
|
||
},
|
||
'post': {
|
||
'accounts': 10.6,
|
||
'accounts/{account_id}/primary': 10.6,
|
||
'accounts/{account_id}/addresses': 10.6,
|
||
'accounts/{account_id}/transactions': 10.6,
|
||
'accounts/{account_id}/transactions/{transaction_id}/complete': 10.6,
|
||
'accounts/{account_id}/transactions/{transaction_id}/resend': 10.6,
|
||
'accounts/{account_id}/buys': 10.6,
|
||
'accounts/{account_id}/buys/{buy_id}/commit': 10.6,
|
||
'accounts/{account_id}/sells': 10.6,
|
||
'accounts/{account_id}/sells/{sell_id}/commit': 10.6,
|
||
'accounts/{account_id}/deposits': 10.6,
|
||
'accounts/{account_id}/deposits/{deposit_id}/commit': 10.6,
|
||
'accounts/{account_id}/withdrawals': 10.6,
|
||
'accounts/{account_id}/withdrawals/{withdrawal_id}/commit': 10.6,
|
||
},
|
||
'put': {
|
||
'accounts/{account_id}': 10.6,
|
||
'user': 10.6,
|
||
},
|
||
'delete': {
|
||
'accounts/{id}': 10.6,
|
||
'accounts/{account_id}/transactions/{transaction_id}': 10.6,
|
||
},
|
||
},
|
||
},
|
||
'v3': {
|
||
'public': {
|
||
'get': {
|
||
'brokerage/time': 3,
|
||
'brokerage/market/product_book': 3,
|
||
'brokerage/market/products': 3,
|
||
'brokerage/market/products/{product_id}': 3,
|
||
'brokerage/market/products/{product_id}/candles': 3,
|
||
'brokerage/market/products/{product_id}/ticker': 3,
|
||
},
|
||
},
|
||
'private': {
|
||
'get': {
|
||
'brokerage/accounts': 1,
|
||
'brokerage/accounts/{account_uuid}': 1,
|
||
'brokerage/orders/historical/batch': 1,
|
||
'brokerage/orders/historical/fills': 1,
|
||
'brokerage/orders/historical/{order_id}': 1,
|
||
'brokerage/products': 3,
|
||
'brokerage/products/{product_id}': 3,
|
||
'brokerage/products/{product_id}/candles': 3,
|
||
'brokerage/products/{product_id}/ticker': 3,
|
||
'brokerage/best_bid_ask': 3,
|
||
'brokerage/product_book': 3,
|
||
'brokerage/transaction_summary': 3,
|
||
'brokerage/portfolios': 1,
|
||
'brokerage/portfolios/{portfolio_uuid}': 1,
|
||
'brokerage/convert/trade/{trade_id}': 1,
|
||
'brokerage/cfm/balance_summary': 1,
|
||
'brokerage/cfm/positions': 1,
|
||
'brokerage/cfm/positions/{product_id}': 1,
|
||
'brokerage/cfm/sweeps': 1,
|
||
'brokerage/intx/portfolio/{portfolio_uuid}': 1,
|
||
'brokerage/intx/positions/{portfolio_uuid}': 1,
|
||
'brokerage/intx/positions/{portfolio_uuid}/{symbol}': 1,
|
||
'brokerage/payment_methods': 1,
|
||
'brokerage/payment_methods/{payment_method_id}': 1,
|
||
'brokerage/key_permissions': 1,
|
||
},
|
||
'post': {
|
||
'brokerage/orders': 1,
|
||
'brokerage/orders/batch_cancel': 1,
|
||
'brokerage/orders/edit': 1,
|
||
'brokerage/orders/edit_preview': 1,
|
||
'brokerage/orders/preview': 1,
|
||
'brokerage/portfolios': 1,
|
||
'brokerage/portfolios/move_funds': 1,
|
||
'brokerage/convert/quote': 1,
|
||
'brokerage/convert/trade/{trade_id}': 1,
|
||
'brokerage/cfm/sweeps/schedule': 1,
|
||
'brokerage/intx/allocate': 1,
|
||
# futures
|
||
'brokerage/orders/close_position': 1,
|
||
},
|
||
'put': {
|
||
'brokerage/portfolios/{portfolio_uuid}': 1,
|
||
},
|
||
'delete': {
|
||
'brokerage/portfolios/{portfolio_uuid}': 1,
|
||
'brokerage/cfm/sweeps': 1,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'fees': {
|
||
'trading': {
|
||
'taker': self.parse_number('0.012'),
|
||
'maker': self.parse_number('0.006'), # {"pricing_tier":"Advanced 1","usd_from":"0","usd_to":"1000","taker_fee_rate":"0.012","maker_fee_rate":"0.006","aop_from":"","aop_to":""}
|
||
'tierBased': True,
|
||
'percentage': True,
|
||
'tiers': {
|
||
'taker': [
|
||
[self.parse_number('0'), self.parse_number('0.006')],
|
||
[self.parse_number('10000'), self.parse_number('0.004')],
|
||
[self.parse_number('50000'), self.parse_number('0.0025')],
|
||
[self.parse_number('100000'), self.parse_number('0.002')],
|
||
[self.parse_number('1000000'), self.parse_number('0.0018')],
|
||
[self.parse_number('15000000'), self.parse_number('0.0016')],
|
||
[self.parse_number('75000000'), self.parse_number('0.0012')],
|
||
[self.parse_number('250000000'), self.parse_number('0.0008')],
|
||
[self.parse_number('400000000'), self.parse_number('0.0005')],
|
||
],
|
||
'maker': [
|
||
[self.parse_number('0'), self.parse_number('0.004')],
|
||
[self.parse_number('10000'), self.parse_number('0.0025')],
|
||
[self.parse_number('50000'), self.parse_number('0.0015')],
|
||
[self.parse_number('100000'), self.parse_number('0.001')],
|
||
[self.parse_number('1000000'), self.parse_number('0.0008')],
|
||
[self.parse_number('15000000'), self.parse_number('0.0006')],
|
||
[self.parse_number('75000000'), self.parse_number('0.0003')],
|
||
[self.parse_number('250000000'), self.parse_number('0.0')],
|
||
[self.parse_number('400000000'), self.parse_number('0.0')],
|
||
],
|
||
},
|
||
},
|
||
},
|
||
'precisionMode': TICK_SIZE,
|
||
'exceptions': {
|
||
'exact': {
|
||
'two_factor_required': AuthenticationError, # 402 When sending money over 2fa limit
|
||
'param_required': ExchangeError, # 400 Missing parameter
|
||
'validation_error': ExchangeError, # 400 Unable to validate POST/PUT
|
||
'invalid_request': ExchangeError, # 400 Invalid request
|
||
'personal_details_required': AuthenticationError, # 400 User’s personal detail required to complete self request
|
||
'identity_verification_required': AuthenticationError, # 400 Identity verification is required to complete self request
|
||
'jumio_verification_required': AuthenticationError, # 400 Document verification is required to complete self request
|
||
'jumio_face_match_verification_required': AuthenticationError, # 400 Document verification including face match is required to complete self request
|
||
'unverified_email': AuthenticationError, # 400 User has not verified their email
|
||
'authentication_error': AuthenticationError, # 401 Invalid auth(generic)
|
||
'invalid_authentication_method': AuthenticationError, # 401 API access is blocked for deleted users.
|
||
'invalid_token': AuthenticationError, # 401 Invalid Oauth token
|
||
'revoked_token': AuthenticationError, # 401 Revoked Oauth token
|
||
'expired_token': AuthenticationError, # 401 Expired Oauth token
|
||
'invalid_scope': AuthenticationError, # 403 User hasn’t authenticated necessary scope
|
||
'not_found': ExchangeError, # 404 Resource not found
|
||
'rate_limit_exceeded': RateLimitExceeded, # 429 Rate limit exceeded
|
||
'internal_server_error': ExchangeError, # 500 Internal server error
|
||
'UNSUPPORTED_ORDER_CONFIGURATION': BadRequest,
|
||
'INSUFFICIENT_FUND': InsufficientFunds,
|
||
'PERMISSION_DENIED': PermissionDenied,
|
||
'INVALID_ARGUMENT': BadRequest,
|
||
'PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE': InvalidOrder,
|
||
'PREVIEW_INSUFFICIENT_FUND': InsufficientFunds,
|
||
},
|
||
'broad': {
|
||
'Insufficient balance in source account': InsufficientFunds,
|
||
'request timestamp expired': InvalidNonce, # {"errors":[{"id":"authentication_error","message":"request timestamp expired"}]}
|
||
'order with self orderID was not found': OrderNotFound, # {"error":"unknown","error_details":"order with self orderID was not found","message":"order with self orderID was not found"}
|
||
},
|
||
},
|
||
'timeframes': {
|
||
'1m': 'ONE_MINUTE',
|
||
'5m': 'FIVE_MINUTE',
|
||
'15m': 'FIFTEEN_MINUTE',
|
||
'30m': 'THIRTY_MINUTE',
|
||
'1h': 'ONE_HOUR',
|
||
'2h': 'TWO_HOUR',
|
||
'6h': 'SIX_HOUR',
|
||
'1d': 'ONE_DAY',
|
||
},
|
||
'commonCurrencies': {
|
||
'CGLD': 'CELO',
|
||
},
|
||
'options': {
|
||
'usePrivate': False,
|
||
'brokerId': 'ccxt',
|
||
'stablePairs': ['BUSD-USD', 'CBETH-ETH', 'DAI-USD', 'GUSD-USD', 'GYEN-USD', 'PAX-USD', 'PAX-USDT', 'USDC-EUR', 'USDC-GBP', 'USDT-EUR', 'USDT-GBP', 'USDT-USD', 'USDT-USDC', 'WBTC-BTC'],
|
||
'fetchCurrencies': {
|
||
'expires': 5000,
|
||
},
|
||
'accounts': [
|
||
'wallet',
|
||
'fiat',
|
||
# 'vault',
|
||
],
|
||
'v3Accounts': [
|
||
'ACCOUNT_TYPE_CRYPTO',
|
||
'ACCOUNT_TYPE_FIAT',
|
||
],
|
||
'networks': {
|
||
'ERC20': 'ethereum',
|
||
'XLM': 'stellar',
|
||
},
|
||
'createMarketBuyOrderRequiresPrice': True,
|
||
'advanced': True, # set to True if using any v3 endpoints from the advanced trade API
|
||
'fetchMarkets': 'fetchMarketsV3', # 'fetchMarketsV3' or 'fetchMarketsV2'
|
||
'timeDifference': 0, # the difference between system clock and exchange server clock
|
||
'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation
|
||
'fetchTicker': 'fetchTickerV3', # 'fetchTickerV3' or 'fetchTickerV2'
|
||
'fetchTickers': 'fetchTickersV3', # 'fetchTickersV3' or 'fetchTickersV2'
|
||
'fetchAccounts': 'fetchAccountsV3', # 'fetchAccountsV3' or 'fetchAccountsV2'
|
||
'fetchBalance': 'v2PrivateGetAccounts', # 'v2PrivateGetAccounts' or 'v3PrivateGetBrokerageAccounts'
|
||
'fetchTime': 'v2PublicGetTime', # 'v2PublicGetTime' or 'v3PublicGetBrokerageTime'
|
||
'user_native_currency': 'USD', # needed to get fees for v3
|
||
},
|
||
'features': {
|
||
'default': {
|
||
'sandbox': False,
|
||
'createOrder': {
|
||
'marginMode': True,
|
||
'triggerPrice': True,
|
||
'triggerPriceType': None,
|
||
'triggerDirection': True,
|
||
'stopLossPrice': True,
|
||
'takeProfitPrice': True,
|
||
'attachedStopLossTakeProfit': None,
|
||
'timeInForce': {
|
||
'IOC': True,
|
||
'FOK': True,
|
||
'PO': True,
|
||
'GTD': True,
|
||
},
|
||
'hedged': False,
|
||
'trailing': False,
|
||
'leverage': True, # todo implement
|
||
'marketBuyByCost': True,
|
||
'marketBuyRequiresPrice': True,
|
||
'selfTradePrevention': False,
|
||
'iceberg': False,
|
||
},
|
||
'createOrders': None,
|
||
'fetchMyTrades': {
|
||
'marginMode': False,
|
||
'limit': 3000,
|
||
'daysBack': None,
|
||
'untilDays': 10000,
|
||
'symbolRequired': False,
|
||
},
|
||
'fetchOrder': {
|
||
'marginMode': False,
|
||
'trigger': False,
|
||
'trailing': False,
|
||
'symbolRequired': False,
|
||
},
|
||
'fetchOpenOrders': {
|
||
'marginMode': False,
|
||
'limit': None,
|
||
'trigger': False,
|
||
'trailing': False,
|
||
'symbolRequired': False,
|
||
},
|
||
'fetchOrders': {
|
||
'marginMode': False,
|
||
'limit': None,
|
||
'daysBack': None,
|
||
'untilDays': 10000,
|
||
'trigger': False,
|
||
'trailing': False,
|
||
'symbolRequired': False,
|
||
},
|
||
'fetchClosedOrders': {
|
||
'marginMode': False,
|
||
'limit': None,
|
||
'daysBack': None,
|
||
'daysBackCanceled': None,
|
||
'untilDays': 10000,
|
||
'trigger': False,
|
||
'trailing': False,
|
||
'symbolRequired': False,
|
||
},
|
||
'fetchOHLCV': {
|
||
'limit': 300,
|
||
},
|
||
},
|
||
'spot': {
|
||
'extends': 'default',
|
||
},
|
||
'swap': {
|
||
'linear': {
|
||
'extends': 'default',
|
||
},
|
||
'inverse': None,
|
||
},
|
||
'future': {
|
||
'linear': {
|
||
'extends': 'default',
|
||
},
|
||
'inverse': None,
|
||
},
|
||
},
|
||
})
|
||
|
||
async def fetch_time(self, params={}) -> Int:
|
||
"""
|
||
fetches the current integer timestamp in milliseconds from the exchange server
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-time#http-request
|
||
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.method]: 'v2PublicGetTime' or 'v3PublicGetBrokerageTime' default is 'v2PublicGetTime'
|
||
:returns int: the current integer timestamp in milliseconds from the exchange server
|
||
"""
|
||
defaultMethod = self.safe_string(self.options, 'fetchTime', 'v2PublicGetTime')
|
||
method = self.safe_string(params, 'method', defaultMethod)
|
||
params = self.omit(params, 'method')
|
||
response = None
|
||
if method == 'v2PublicGetTime':
|
||
response = await self.v2PublicGetTime(params)
|
||
#
|
||
# {
|
||
# "data": {
|
||
# "epoch": 1589295679,
|
||
# "iso": "2020-05-12T15:01:19Z"
|
||
# }
|
||
# }
|
||
#
|
||
response = self.safe_dict(response, 'data', {})
|
||
else:
|
||
response = await self.v3PublicGetBrokerageTime(params)
|
||
#
|
||
# {
|
||
# "iso": "2024-02-27T03:37:14Z",
|
||
# "epochSeconds": "1709005034",
|
||
# "epochMillis": "1709005034333"
|
||
# }
|
||
#
|
||
return self.safe_timestamp_2(response, 'epoch', 'epochSeconds')
|
||
|
||
async def fetch_accounts(self, params={}) -> List[Account]:
|
||
"""
|
||
fetch all the accounts associated with a profile
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getaccounts
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts
|
||
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
||
:returns dict: a dictionary of `account structures <https://docs.ccxt.com/#/?id=account-structure>` indexed by the account type
|
||
"""
|
||
method = self.safe_string(self.options, 'fetchAccounts', 'fetchAccountsV3')
|
||
if method == 'fetchAccountsV3':
|
||
return await self.fetch_accounts_v3(params)
|
||
return await self.fetch_accounts_v2(params)
|
||
|
||
async def fetch_accounts_v2(self, params={}) -> List[Account]:
|
||
await self.load_markets()
|
||
paginate = False
|
||
paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'next_starting_after', 'starting_after', None, 100)
|
||
request: dict = {
|
||
'limit': 100,
|
||
}
|
||
response = await self.v2PrivateGetAccounts(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "pagination": {
|
||
# "ending_before": null,
|
||
# "starting_after": null,
|
||
# "previous_ending_before": null,
|
||
# "next_starting_after": null,
|
||
# "limit": 244,
|
||
# "order": "desc",
|
||
# "previous_uri": null,
|
||
# "next_uri": null
|
||
# },
|
||
# "data": [
|
||
# {
|
||
# "id": "XLM",
|
||
# "name": "XLM Wallet",
|
||
# "primary": False,
|
||
# "type": "wallet",
|
||
# "currency": {
|
||
# "code": "XLM",
|
||
# "name": "Stellar Lumens",
|
||
# "color": "#000000",
|
||
# "sort_index": 127,
|
||
# "exponent": 7,
|
||
# "type": "crypto",
|
||
# "address_regex": "^G[A-Z2-7]{55}$",
|
||
# "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f",
|
||
# "destination_tag_name": "XLM Memo ID",
|
||
# "destination_tag_regex": "^[-~]{1,28}$"
|
||
# },
|
||
# "balance": {
|
||
# "amount": "0.0000000",
|
||
# "currency": "XLM"
|
||
# },
|
||
# "created_at": null,
|
||
# "updated_at": null,
|
||
# "resource": "account",
|
||
# "resource_path": "/v2/accounts/XLM",
|
||
# "allow_deposits": True,
|
||
# "allow_withdrawals": True
|
||
# },
|
||
# ]
|
||
# }
|
||
#
|
||
data = self.safe_list(response, 'data', [])
|
||
pagination = self.safe_dict(response, 'pagination', {})
|
||
cursor = self.safe_string(pagination, 'next_starting_after')
|
||
accounts = self.safe_list(response, 'data', [])
|
||
length = len(accounts)
|
||
lastIndex = length - 1
|
||
last = self.safe_dict(accounts, lastIndex)
|
||
if (cursor is not None) and (cursor != ''):
|
||
last['next_starting_after'] = cursor
|
||
accounts[lastIndex] = last
|
||
return self.parse_accounts(data, params)
|
||
|
||
async def fetch_accounts_v3(self, params={}) -> List[Account]:
|
||
await self.load_markets()
|
||
paginate = False
|
||
paginate, params = self.handle_option_and_params(params, 'fetchAccounts', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchAccounts', None, None, None, params, 'cursor', 'cursor', None, 250)
|
||
request: dict = {
|
||
'limit': 250,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageAccounts(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "accounts": [
|
||
# {
|
||
# "uuid": "11111111-1111-1111-1111-111111111111",
|
||
# "name": "USDC Wallet",
|
||
# "currency": "USDC",
|
||
# "available_balance": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "default": True,
|
||
# "active": True,
|
||
# "created_at": "2023-01-04T06:20:06.456Z",
|
||
# "updated_at": "2023-01-04T06:20:07.181Z",
|
||
# "deleted_at": null,
|
||
# "type": "ACCOUNT_TYPE_CRYPTO",
|
||
# "ready": False,
|
||
# "hold": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# }
|
||
# },
|
||
# ...
|
||
# ],
|
||
# "has_next": False,
|
||
# "cursor": "",
|
||
# "size": 9
|
||
# }
|
||
#
|
||
accounts = self.safe_list(response, 'accounts', [])
|
||
length = len(accounts)
|
||
lastIndex = length - 1
|
||
last = self.safe_dict(accounts, lastIndex)
|
||
cursor = self.safe_string(response, 'cursor')
|
||
if (cursor is not None) and (cursor != ''):
|
||
last['cursor'] = cursor
|
||
accounts[lastIndex] = last
|
||
return self.parse_accounts(accounts, params)
|
||
|
||
async def fetch_portfolios(self, params={}) -> List[Account]:
|
||
"""
|
||
fetch all the portfolios
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios
|
||
|
||
: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
|
||
"""
|
||
response = await self.v3PrivateGetBrokeragePortfolios(params)
|
||
portfolios = self.safe_list(response, 'portfolios', [])
|
||
result = []
|
||
for i in range(0, len(portfolios)):
|
||
portfolio = portfolios[i]
|
||
result.append({
|
||
'id': self.safe_string(portfolio, 'uuid'),
|
||
'type': self.safe_string(portfolio, 'type'),
|
||
'code': None,
|
||
'info': portfolio,
|
||
})
|
||
return result
|
||
|
||
def parse_account(self, account):
|
||
#
|
||
# fetchAccountsV2
|
||
#
|
||
# {
|
||
# "id": "XLM",
|
||
# "name": "XLM Wallet",
|
||
# "primary": False,
|
||
# "type": "wallet",
|
||
# "currency": {
|
||
# "code": "XLM",
|
||
# "name": "Stellar Lumens",
|
||
# "color": "#000000",
|
||
# "sort_index": 127,
|
||
# "exponent": 7,
|
||
# "type": "crypto",
|
||
# "address_regex": "^G[A-Z2-7]{55}$",
|
||
# "asset_id": "13b83335-5ede-595b-821e-5bcdfa80560f",
|
||
# "destination_tag_name": "XLM Memo ID",
|
||
# "destination_tag_regex": "^[-~]{1,28}$"
|
||
# },
|
||
# "balance": {
|
||
# "amount": "0.0000000",
|
||
# "currency": "XLM"
|
||
# },
|
||
# "created_at": null,
|
||
# "updated_at": null,
|
||
# "resource": "account",
|
||
# "resource_path": "/v2/accounts/XLM",
|
||
# "allow_deposits": True,
|
||
# "allow_withdrawals": True
|
||
# }
|
||
#
|
||
# fetchAccountsV3
|
||
#
|
||
# {
|
||
# "uuid": "11111111-1111-1111-1111-111111111111",
|
||
# "name": "USDC Wallet",
|
||
# "currency": "USDC",
|
||
# "available_balance": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "default": True,
|
||
# "active": True,
|
||
# "created_at": "2023-01-04T06:20:06.456Z",
|
||
# "updated_at": "2023-01-04T06:20:07.181Z",
|
||
# "deleted_at": null,
|
||
# "type": "ACCOUNT_TYPE_CRYPTO",
|
||
# "ready": False,
|
||
# "hold": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# }
|
||
# }
|
||
#
|
||
active = self.safe_bool(account, 'active')
|
||
currencyIdV3 = self.safe_string(account, 'currency')
|
||
currency = self.safe_dict(account, 'currency', {})
|
||
currencyId = self.safe_string(currency, 'code', currencyIdV3)
|
||
typeV3 = self.safe_string(account, 'name')
|
||
typeV2 = self.safe_string(account, 'type')
|
||
parts = typeV3.split(' ')
|
||
return {
|
||
'id': self.safe_string_2(account, 'id', 'uuid'),
|
||
'type': self.safe_string_lower(parts, 1) if (active is not None) else typeV2,
|
||
'code': self.safe_currency_code(currencyId),
|
||
'info': account,
|
||
}
|
||
|
||
async def create_deposit_address(self, code: str, params={}) -> DepositAddress:
|
||
"""
|
||
create a currency deposit address
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-addresses#create-address
|
||
|
||
: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>`
|
||
"""
|
||
accountId = self.safe_string(params, 'account_id')
|
||
params = self.omit(params, 'account_id')
|
||
if accountId is None:
|
||
await self.load_accounts()
|
||
for i in range(0, len(self.accounts)):
|
||
account = self.accounts[i]
|
||
if account['code'] == code and account['type'] == 'wallet':
|
||
accountId = account['id']
|
||
break
|
||
if accountId is None:
|
||
raise ExchangeError(self.id + ' createDepositAddress() could not find the account with matching currency code ' + code + ', specify an `account_id` extra param to target specific wallet')
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
}
|
||
response = await self.v2PrivatePostAccountsAccountIdAddresses(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "data": {
|
||
# "id": "05b1ebbf-9438-5dd4-b297-2ddedc98d0e4",
|
||
# "address": "coinbasebase",
|
||
# "address_info": {
|
||
# "address": "coinbasebase",
|
||
# "destination_tag": "287594668"
|
||
# },
|
||
# "name": null,
|
||
# "created_at": "2019-07-01T14:39:29Z",
|
||
# "updated_at": "2019-07-01T14:39:29Z",
|
||
# "network": "eosio",
|
||
# "uri_scheme": "eosio",
|
||
# "resource": "address",
|
||
# "resource_path": "/v2/accounts/14cfc769-e852-52f3-b831-711c104d194c/addresses/05b1ebbf-9438-5dd4-b297-2ddedc98d0e4",
|
||
# "warnings": [
|
||
# {
|
||
# "title": "Only send EOS(EOS) to self address",
|
||
# "details": "Sending any other cryptocurrency will result in permanent loss.",
|
||
# "image_url": "https://dynamic-assets.coinbase.com/deaca3d47b10ed4a91a872e9618706eec34081127762d88f2476ac8e99ada4b48525a9565cf2206d18c04053f278f693434af4d4629ca084a9d01b7a286a7e26/asset_icons/1f8489bb280fb0a0fd643c1161312ba49655040e9aaaced5f9ad3eeaf868eadc.png"
|
||
# },
|
||
# {
|
||
# "title": "Both an address and EOS memo are required to receive EOS",
|
||
# "details": "If you send funds without an EOS memo or with an incorrect EOS memo, your funds cannot be credited to your account.",
|
||
# "image_url": "https://www.coinbase.com/assets/receive-warning-2f3269d83547a7748fb39d6e0c1c393aee26669bfea6b9f12718094a1abff155.png"
|
||
# }
|
||
# ],
|
||
# "warning_title": "Only send EOS(EOS) to self address",
|
||
# "warning_details": "Sending any other cryptocurrency will result in permanent loss.",
|
||
# "destination_tag": "287594668",
|
||
# "deposit_uri": "eosio:coinbasebase?dt=287594668",
|
||
# "callback_url": null
|
||
# }
|
||
# }
|
||
#
|
||
data = self.safe_dict(response, 'data', {})
|
||
tag = self.safe_string(data, 'destination_tag')
|
||
address = self.safe_string(data, 'address')
|
||
return {
|
||
'currency': code,
|
||
'tag': tag,
|
||
'address': address,
|
||
'network': None,
|
||
'info': response,
|
||
}
|
||
|
||
async def fetch_my_sells(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
||
"""
|
||
@ignore
|
||
fetch sells
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-sells#list-sells
|
||
|
||
:param str symbol: not used by coinbase fetchMySells()
|
||
:param int [since]: timestamp in ms of the earliest sell, default is None
|
||
:param int [limit]: max number of sells to return, default is 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>`
|
||
"""
|
||
# v2 did't have an endpoint for all historical trades
|
||
request = self.prepare_account_request(limit, params)
|
||
await self.load_markets()
|
||
query = self.omit(params, ['account_id', 'accountId'])
|
||
sells = await self.v2PrivateGetAccountsAccountIdSells(self.extend(request, query))
|
||
return self.parse_trades(sells['data'], None, since, limit)
|
||
|
||
async def fetch_my_buys(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
||
"""
|
||
@ignore
|
||
fetch buys
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-buys#list-buys
|
||
|
||
:param str symbol: not used by coinbase fetchMyBuys()
|
||
:param int [since]: timestamp in ms of the earliest buy, default is None
|
||
:param int [limit]: max number of buys to return, default is 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>`
|
||
"""
|
||
# v2 did't have an endpoint for all historical trades
|
||
request = self.prepare_account_request(limit, params)
|
||
await self.load_markets()
|
||
query = self.omit(params, ['account_id', 'accountId'])
|
||
buys = await self.v2PrivateGetAccountsAccountIdBuys(self.extend(request, query))
|
||
return self.parse_trades(buys['data'], None, since, limit)
|
||
|
||
async def fetch_transactions_with_method(self, method, code: Str = None, since: Int = None, limit: Int = None, params={}):
|
||
request = None
|
||
request, params = await self.prepare_account_request_with_currency_code(code, limit, params)
|
||
await self.load_markets()
|
||
response = await getattr(self, method)(self.extend(request, params))
|
||
return self.parse_transactions(response['data'], None, 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. Won't return crypto withdrawals. Use fetchLedger for those.
|
||
|
||
https://docs.cdp.coinbase.com/coinbase-app/docs/api-withdrawals#list-withdrawals
|
||
|
||
:param str code: unified currency code
|
||
:param int [since]: the earliest time in ms to fetch withdrawals for
|
||
:param int [limit]: the maximum number of withdrawals structures to retrieve
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.currencyType]: "fiat" or "crypto"
|
||
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
||
"""
|
||
currencyType = None
|
||
currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType')
|
||
if currencyType == 'crypto':
|
||
results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params)
|
||
return self.filter_by_array(results, 'type', 'withdrawal', False)
|
||
return await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdWithdrawals', code, since, limit, params)
|
||
|
||
async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
||
"""
|
||
Fetch all fiat deposits made to an account. Won't return crypto deposits or staking rewards. Use fetchLedger for those.
|
||
|
||
https://docs.cdp.coinbase.com/coinbase-app/docs/api-deposits#list-deposits
|
||
|
||
:param str code: unified currency code
|
||
:param int [since]: the earliest time in ms to fetch deposits for
|
||
:param int [limit]: the maximum number of deposits structures to retrieve
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.currencyType]: "fiat" or "crypto"
|
||
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
||
"""
|
||
currencyType = None
|
||
currencyType, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'currencyType')
|
||
if currencyType == 'crypto':
|
||
results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params)
|
||
return self.filter_by_array(results, 'type', 'deposit', False)
|
||
return await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdDeposits', code, since, limit, params)
|
||
|
||
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.cdp.coinbase.com/coinbase-app/docs/api-transactions
|
||
|
||
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
||
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
||
:param int [limit]: max number of deposit/withdrawals to return, default = 50, Min: 1, Max: 100
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:returns dict: a list of `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
results = await self.fetch_transactions_with_method('v2PrivateGetAccountsAccountIdTransactions', code, since, limit, params)
|
||
return self.filter_by_array(results, 'type', ['deposit', 'withdrawal'], False)
|
||
|
||
def parse_transaction_status(self, status: Str):
|
||
statuses: dict = {
|
||
'created': 'pending',
|
||
'completed': 'ok',
|
||
'canceled': 'canceled',
|
||
}
|
||
return self.safe_string(statuses, status, status)
|
||
|
||
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
||
#
|
||
# fiat deposit
|
||
#
|
||
# {
|
||
# "id": "f34c19f3-b730-5e3d-9f72",
|
||
# "status": "completed",
|
||
# "payment_method": {
|
||
# "id": "a022b31d-f9c7-5043-98f2",
|
||
# "resource": "payment_method",
|
||
# "resource_path": "/v2/payment-methods/a022b31d-f9c7-5043-98f2"
|
||
# },
|
||
# "transaction": {
|
||
# "id": "04ed4113-3732-5b0c-af86-b1d2146977d0",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86"
|
||
# },
|
||
# "user_reference": "2VTYTH",
|
||
# "created_at": "2017-02-09T07:01:18Z",
|
||
# "updated_at": "2017-02-09T07:01:26Z",
|
||
# "resource": "deposit",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72",
|
||
# "committed": True,
|
||
# "payout_at": "2017-02-12T07:01:17Z",
|
||
# "instant": False,
|
||
# "fee": {"amount": "0.00", "currency": "EUR"},
|
||
# "amount": {"amount": "114.02", "currency": "EUR"},
|
||
# "subtotal": {"amount": "114.02", "currency": "EUR"},
|
||
# "hold_until": null,
|
||
# "hold_days": 0,
|
||
# "hold_business_days": 0,
|
||
# "next_step": null
|
||
# }
|
||
#
|
||
# fiat_withdrawal
|
||
#
|
||
# {
|
||
# "id": "cfcc3b4a-eeb6-5e8c-8058",
|
||
# "status": "completed",
|
||
# "payment_method": {
|
||
# "id": "8b94cfa4-f7fd-5a12-a76a",
|
||
# "resource": "payment_method",
|
||
# "resource_path": "/v2/payment-methods/8b94cfa4-f7fd-5a12-a76a"
|
||
# },
|
||
# "transaction": {
|
||
# "id": "fcc2550b-5104-5f83-a444",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/fcc2550b-5104-5f83-a444"
|
||
# },
|
||
# "user_reference": "MEUGK",
|
||
# "created_at": "2018-07-26T08:55:12Z",
|
||
# "updated_at": "2018-07-26T08:58:18Z",
|
||
# "resource": "withdrawal",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/cfcc3b4a-eeb6-5e8c-8058",
|
||
# "committed": True,
|
||
# "payout_at": "2018-07-31T08:55:12Z",
|
||
# "instant": False,
|
||
# "fee": {"amount": "0.15", "currency": "EUR"},
|
||
# "amount": {"amount": "13130.69", "currency": "EUR"},
|
||
# "subtotal": {"amount": "13130.84", "currency": "EUR"},
|
||
# "idem": "e549dee5-63ed-4e79-8a96",
|
||
# "next_step": null
|
||
# }
|
||
#
|
||
# withdraw
|
||
#
|
||
# {
|
||
# "id": "a1794ecf-5693-55fa-70cf-ef731748ed82",
|
||
# "type": "send",
|
||
# "status": "pending",
|
||
# "amount": {
|
||
# "amount": "-14.008308",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "native_amount": {
|
||
# "amount": "-18.74",
|
||
# "currency": "CAD"
|
||
# },
|
||
# "description": null,
|
||
# "created_at": "2024-01-12T01:27:31Z",
|
||
# "updated_at": "2024-01-12T01:27:31Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82",
|
||
# "instant_exchange": False,
|
||
# "network": {
|
||
# "status": "pending",
|
||
# "status_description": "Pending(est. less than 10 minutes)",
|
||
# "transaction_fee": {
|
||
# "amount": "4.008308",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "transaction_amount": {
|
||
# "amount": "10.000000",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "confirmations": 0
|
||
# },
|
||
# "to": {
|
||
# "resource": "ethereum_address",
|
||
# "address": "0x9...",
|
||
# "currency": "USDC",
|
||
# "address_info": {
|
||
# "address": "0x9..."
|
||
# }
|
||
# },
|
||
# "idem": "748d8591-dg9a-7831-a45b-crd61dg78762",
|
||
# "details": {
|
||
# "title": "Sent USDC",
|
||
# "subtitle": "To USDC address on Ethereum network",
|
||
# "header": "Sent 14.008308 USDC($18.74)",
|
||
# "health": "warning"
|
||
# },
|
||
# "hide_native_amount": False
|
||
# }
|
||
#
|
||
#
|
||
# crypto deposit & withdrawal(using `/transactions` endpoint)
|
||
# {
|
||
# "amount": {
|
||
# "amount": "0.00014200",(negative for withdrawal)
|
||
# "currency": "BTC"
|
||
# },
|
||
# "created_at": "2024-03-29T15:48:30Z",
|
||
# "id": "0031a605-241d-514d-a97b-d4b99f3225d3",
|
||
# "idem": "092a979b-017e-4403-940a-2ca57811f442", # field present only in case of withdrawal
|
||
# "native_amount": {
|
||
# "amount": "9.85",(negative for withdrawal)
|
||
# "currency": "USD"
|
||
# },
|
||
# "network": {
|
||
# "status": "pending", # if status is `off_blockchain` then no more other fields are hasattr(self, present) object
|
||
# "hash": "5jYuvrNsvX2DZoMnzGYzVpYxJLfYu4GSK3xetG1H5LHrSovsuFCFYdFMwNRoiht3s6fBk92MM8QLLnz65xuEFTrE",
|
||
# "network_name": "solana",
|
||
# "transaction_fee": {
|
||
# "amount": "0.000100000",
|
||
# "currency": "SOL"
|
||
# }
|
||
# },
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/dc504b1c-248e-5b68-a3b0-b991f7fa84e6/transactions/0031a605-241d-514d-a97b-d4b99f3225d3",
|
||
# "status": "completed",
|
||
# "type": "send",
|
||
# "from": { # in some cases, field might be present for deposit
|
||
# "id": "7fd10cd7-b091-5cee-ba41-c29e49a7cccf",
|
||
# "name": "Coinbase",
|
||
# "resource": "user"
|
||
# },
|
||
# "to": { # field only present for withdrawal
|
||
# "address": "5HA12BNthAvBwNYARYf9y5MqqCpB4qhCNFCs1Qw48ACE",
|
||
# "resource": "address"
|
||
# },
|
||
# "description": "C3 - One Time BTC Credit . Reference Case # 123.", # in some cases, field might be present for deposit
|
||
# }
|
||
#
|
||
transactionType = self.safe_string(transaction, 'type')
|
||
amountAndCurrencyObject = None
|
||
feeObject = None
|
||
network = self.safe_dict(transaction, 'network', {})
|
||
if transactionType == 'send':
|
||
amountAndCurrencyObject = self.safe_dict(network, 'transaction_amount')
|
||
feeObject = self.safe_dict(network, 'transaction_fee', {})
|
||
else:
|
||
amountAndCurrencyObject = self.safe_dict(transaction, 'subtotal')
|
||
feeObject = self.safe_dict(transaction, 'fee', {})
|
||
if amountAndCurrencyObject is None:
|
||
amountAndCurrencyObject = self.safe_dict(transaction, 'amount')
|
||
amountString = self.safe_string(amountAndCurrencyObject, 'amount')
|
||
amountStringAbs = Precise.string_abs(amountString)
|
||
status = self.parse_transaction_status(self.safe_string(transaction, 'status'))
|
||
if status is None:
|
||
committed = self.safe_bool(transaction, 'committed')
|
||
status = 'ok' if committed else 'pending'
|
||
id = self.safe_string(transaction, 'id')
|
||
currencyId = self.safe_string(amountAndCurrencyObject, 'currency')
|
||
feeCurrencyId = self.safe_string(feeObject, 'currency')
|
||
datetime = self.safe_string(transaction, 'created_at')
|
||
resource = self.safe_string(transaction, 'resource')
|
||
type = resource
|
||
if not self.in_array(type, ['deposit', 'withdrawal']):
|
||
if Precise.string_gt(amountString, '0'):
|
||
type = 'deposit'
|
||
elif Precise.string_lt(amountString, '0'):
|
||
type = 'withdrawal'
|
||
toObject = self.safe_dict(transaction, 'to')
|
||
addressTo = self.safe_string(toObject, 'address')
|
||
networkId = self.safe_string(network, 'network_name')
|
||
return {
|
||
'info': transaction,
|
||
'id': id,
|
||
'txid': self.safe_string(network, 'hash', id),
|
||
'timestamp': self.parse8601(datetime),
|
||
'datetime': datetime,
|
||
'network': self.network_id_to_code(networkId),
|
||
'address': addressTo,
|
||
'addressTo': addressTo,
|
||
'addressFrom': None,
|
||
'tag': None,
|
||
'tagTo': None,
|
||
'tagFrom': None,
|
||
'type': type,
|
||
'amount': self.parse_number(amountStringAbs),
|
||
'currency': self.safe_currency_code(currencyId, currency),
|
||
'status': status,
|
||
'updated': self.parse8601(self.safe_string(transaction, 'updated_at')),
|
||
'fee': {
|
||
'cost': self.safe_number(feeObject, 'amount'),
|
||
'currency': self.safe_currency_code(feeCurrencyId),
|
||
},
|
||
}
|
||
|
||
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
||
#
|
||
# fetchMyBuys, fetchMySells
|
||
#
|
||
# {
|
||
# "id": "67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "status": "completed",
|
||
# "payment_method": {
|
||
# "id": "83562370-3e5c-51db-87da-752af5ab9559",
|
||
# "resource": "payment_method",
|
||
# "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559"
|
||
# },
|
||
# "transaction": {
|
||
# "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a"
|
||
# },
|
||
# "amount": {"amount": "1.00000000", "currency": "BTC"},
|
||
# "total": {"amount": "10.25", "currency": "USD"},
|
||
# "subtotal": {"amount": "10.10", "currency": "USD"},
|
||
# "created_at": "2015-01-31T20:49:02Z",
|
||
# "updated_at": "2015-02-11T16:54:02-08:00",
|
||
# "resource": "buy",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/buys/67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "committed": True,
|
||
# "instant": False,
|
||
# "fee": {"amount": "0.15", "currency": "USD"},
|
||
# "payout_at": "2015-02-18T16:54:00-08:00"
|
||
# }
|
||
#
|
||
# fetchTrades
|
||
#
|
||
# {
|
||
# "trade_id": "10092327",
|
||
# "product_id": "BTC-USDT",
|
||
# "price": "17488.12",
|
||
# "size": "0.0000623",
|
||
# "time": "2023-01-11T00:52:37.557001Z",
|
||
# "side": "BUY",
|
||
# "bid": "",
|
||
# "ask": ""
|
||
# }
|
||
#
|
||
# fetchMyTrades
|
||
#
|
||
# {
|
||
# "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010",
|
||
# "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1",
|
||
# "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8",
|
||
# "trade_time": "2023-01-18T01:37:38.091377090Z",
|
||
# "trade_type": "FILL",
|
||
# "price": "21220.64",
|
||
# "size": "0.0046830664333996",
|
||
# "commission": "0.0000280983986004",
|
||
# "product_id": "BTC-USDT",
|
||
# "sequence_timestamp": "2023-01-18T01:37:38.092520Z",
|
||
# "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR",
|
||
# "size_in_quote": True,
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "side": "BUY"
|
||
# }
|
||
#
|
||
symbol = None
|
||
totalObject = self.safe_dict(trade, 'total', {})
|
||
amountObject = self.safe_dict(trade, 'amount', {})
|
||
subtotalObject = self.safe_dict(trade, 'subtotal', {})
|
||
feeObject = self.safe_dict(trade, 'fee', {})
|
||
marketId = self.safe_string(trade, 'product_id')
|
||
market = self.safe_market(marketId, market, '-')
|
||
if market is not None:
|
||
symbol = market['symbol']
|
||
else:
|
||
baseId = self.safe_string(amountObject, 'currency')
|
||
quoteId = self.safe_string(totalObject, 'currency')
|
||
if (baseId is not None) and (quoteId is not None):
|
||
base = self.safe_currency_code(baseId)
|
||
quote = self.safe_currency_code(quoteId)
|
||
symbol = base + '/' + quote
|
||
sizeInQuote = self.safe_bool(trade, 'size_in_quote')
|
||
v3Price = self.safe_string(trade, 'price')
|
||
v3Cost = None
|
||
v3Amount = self.safe_string(trade, 'size')
|
||
if sizeInQuote:
|
||
# calculate base size
|
||
v3Cost = v3Amount
|
||
v3Amount = Precise.string_div(v3Amount, v3Price)
|
||
v3FeeCost = self.safe_string(trade, 'commission')
|
||
amountString = self.safe_string(amountObject, 'amount', v3Amount)
|
||
costString = self.safe_string(subtotalObject, 'amount', v3Cost)
|
||
priceString = None
|
||
cost = None
|
||
if (costString is not None) and (amountString is not None):
|
||
priceString = Precise.string_div(costString, amountString)
|
||
else:
|
||
priceString = v3Price
|
||
if (priceString is not None) and (amountString is not None):
|
||
cost = Precise.string_mul(priceString, amountString)
|
||
else:
|
||
cost = costString
|
||
feeCurrencyId = self.safe_string(feeObject, 'currency')
|
||
feeCost = self.safe_number(feeObject, 'amount', self.parse_number(v3FeeCost))
|
||
if (feeCurrencyId is None) and (market is not None) and (feeCost is not None):
|
||
feeCurrencyId = market['quote']
|
||
datetime = self.safe_string_n(trade, ['created_at', 'trade_time', 'time'])
|
||
side = self.safe_string_lower_2(trade, 'resource', 'side')
|
||
takerOrMaker = self.safe_string_lower(trade, 'liquidity_indicator')
|
||
return self.safe_trade({
|
||
'info': trade,
|
||
'id': self.safe_string_2(trade, 'id', 'trade_id'),
|
||
'order': self.safe_string(trade, 'order_id'),
|
||
'timestamp': self.parse8601(datetime),
|
||
'datetime': datetime,
|
||
'symbol': symbol,
|
||
'type': None,
|
||
'side': None if (side == 'unknown_order_side') else side,
|
||
'takerOrMaker': None if (takerOrMaker == 'unknown_liquidity_indicator') else takerOrMaker,
|
||
'price': priceString,
|
||
'amount': amountString,
|
||
'cost': cost,
|
||
'fee': {
|
||
'cost': feeCost,
|
||
'currency': self.safe_currency_code(feeCurrencyId),
|
||
},
|
||
})
|
||
|
||
async def fetch_markets(self, params={}) -> List[Market]:
|
||
"""
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproducts
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates
|
||
|
||
retrieves data on all markets for coinbase
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.usePrivate]: use private endpoint for fetching markets
|
||
:returns dict[]: an array of objects representing market data
|
||
"""
|
||
if self.options['adjustForTimeDifference']:
|
||
await self.load_time_difference()
|
||
method = self.safe_string(self.options, 'fetchMarkets', 'fetchMarketsV3')
|
||
if method == 'fetchMarketsV3':
|
||
return await self.fetch_markets_v3(params)
|
||
return await self.fetch_markets_v2(params)
|
||
|
||
async def fetch_markets_v2(self, params={}) -> List[Market]:
|
||
response = await self.fetch_currencies_from_cache(params)
|
||
currencies = self.safe_dict(response, 'currencies', {})
|
||
exchangeRates = self.safe_dict(response, 'exchangeRates', {})
|
||
data = self.safe_list(currencies, 'data', [])
|
||
dataById = self.index_by(data, 'id')
|
||
rates = self.safe_dict(self.safe_dict(exchangeRates, 'data', {}), 'rates', {})
|
||
baseIds = list(rates.keys())
|
||
result = []
|
||
for i in range(0, len(baseIds)):
|
||
baseId = baseIds[i]
|
||
base = self.safe_currency_code(baseId)
|
||
type = 'fiat' if (baseId in dataById) else 'crypto'
|
||
# https://github.com/ccxt/ccxt/issues/6066
|
||
if type == 'crypto':
|
||
for j in range(0, len(data)):
|
||
quoteCurrency = data[j]
|
||
quoteId = self.safe_string(quoteCurrency, 'id')
|
||
quote = self.safe_currency_code(quoteId)
|
||
result.append(self.safe_market_structure({
|
||
'id': baseId + '-' + quoteId,
|
||
'symbol': base + '/' + quote,
|
||
'base': base,
|
||
'quote': quote,
|
||
'settle': None,
|
||
'baseId': baseId,
|
||
'quoteId': quoteId,
|
||
'settleId': None,
|
||
'type': 'spot',
|
||
'spot': True,
|
||
'margin': False,
|
||
'swap': False,
|
||
'future': False,
|
||
'option': False,
|
||
'active': None,
|
||
'contract': False,
|
||
'linear': None,
|
||
'inverse': None,
|
||
'contractSize': None,
|
||
'expiry': None,
|
||
'expiryDatetime': None,
|
||
'strike': None,
|
||
'optionType': None,
|
||
'precision': {
|
||
'amount': None,
|
||
'price': None,
|
||
},
|
||
'limits': {
|
||
'leverage': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'amount': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'price': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'cost': {
|
||
'min': self.safe_number(quoteCurrency, 'min_size'),
|
||
'max': None,
|
||
},
|
||
},
|
||
'info': quoteCurrency,
|
||
}))
|
||
return result
|
||
|
||
async def fetch_markets_v3(self, params={}) -> List[Market]:
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchMarkets', 'usePrivate', False)
|
||
spotUnresolvedPromises = []
|
||
if usePrivate:
|
||
spotUnresolvedPromises.append(self.v3PrivateGetBrokerageProducts(params))
|
||
else:
|
||
spotUnresolvedPromises.append(self.v3PublicGetBrokerageMarketProducts(params))
|
||
#
|
||
# {
|
||
# products: [
|
||
# {
|
||
# product_id: 'BTC-USD',
|
||
# price: '67060',
|
||
# price_percentage_change_24h: '3.30054960636883',
|
||
# volume_24h: '10967.87426597',
|
||
# volume_percentage_change_24h: '141.73048325503036',
|
||
# base_increment: '0.00000001',
|
||
# quote_increment: '0.01',
|
||
# quote_min_size: '1',
|
||
# quote_max_size: '150000000',
|
||
# base_min_size: '0.00000001',
|
||
# base_max_size: '3400',
|
||
# base_name: 'Bitcoin',
|
||
# quote_name: 'US Dollar',
|
||
# watched: False,
|
||
# is_disabled: False,
|
||
# new: False,
|
||
# status: 'online',
|
||
# cancel_only: False,
|
||
# limit_only: False,
|
||
# post_only: False,
|
||
# trading_disabled: False,
|
||
# auction_mode: False,
|
||
# product_type: 'SPOT',
|
||
# quote_currency_id: 'USD',
|
||
# base_currency_id: 'BTC',
|
||
# fcm_trading_session_details: null,
|
||
# mid_market_price: '',
|
||
# alias: '',
|
||
# alias_to: ['BTC-USDC'],
|
||
# base_display_symbol: 'BTC',
|
||
# quote_display_symbol: 'USD',
|
||
# view_only: False,
|
||
# price_increment: '0.01',
|
||
# display_name: 'BTC-USD',
|
||
# product_venue: 'CBE'
|
||
# },
|
||
# ...
|
||
# ],
|
||
# num_products: '646'
|
||
# }
|
||
#
|
||
if self.check_required_credentials(False):
|
||
spotUnresolvedPromises.append(self.v3PrivateGetBrokerageTransactionSummary(params))
|
||
#
|
||
# {
|
||
# total_volume: '9.995989116664404',
|
||
# total_fees: '0.07996791093331522',
|
||
# fee_tier: {
|
||
# pricing_tier: 'Advanced 1',
|
||
# usd_from: '0',
|
||
# usd_to: '1000',
|
||
# taker_fee_rate: '0.008',
|
||
# maker_fee_rate: '0.006',
|
||
# aop_from: '',
|
||
# aop_to: ''
|
||
# },
|
||
# margin_rate: null,
|
||
# goods_and_services_tax: null,
|
||
# advanced_trade_only_volume: '9.995989116664404',
|
||
# advanced_trade_only_fees: '0.07996791093331522',
|
||
# coinbase_pro_volume: '0',
|
||
# coinbase_pro_fees: '0',
|
||
# total_balance: '',
|
||
# has_promo_fee: False
|
||
# }
|
||
#
|
||
unresolvedContractPromises = []
|
||
try:
|
||
unresolvedContractPromises = [
|
||
self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE'})),
|
||
self.v3PublicGetBrokerageMarketProducts(self.extend(params, {'product_type': 'FUTURE', 'contract_expiry_type': 'PERPETUAL'})),
|
||
]
|
||
except Exception as e:
|
||
unresolvedContractPromises = [] # the sync version of ccxt won't have the promise.all line so the request is made here. Some users can't access perpetual products
|
||
promises = await asyncio.gather(*spotUnresolvedPromises)
|
||
contractPromises = None
|
||
try:
|
||
contractPromises = await asyncio.gather(*unresolvedContractPromises) # some users don't have access to contracts
|
||
except Exception as e:
|
||
contractPromises = []
|
||
spot = self.safe_dict(promises, 0, {})
|
||
fees = self.safe_dict(promises, 1, {})
|
||
expiringFutures = self.safe_dict(contractPromises, 0, {})
|
||
perpetualFutures = self.safe_dict(contractPromises, 1, {})
|
||
expiringFees = self.safe_dict(contractPromises, 0, {})
|
||
perpetualFees = self.safe_dict(contractPromises, 1, {})
|
||
#
|
||
# {
|
||
# "total_volume": 0,
|
||
# "total_fees": 0,
|
||
# "fee_tier": {
|
||
# "pricing_tier": "",
|
||
# "usd_from": "0",
|
||
# "usd_to": "10000",
|
||
# "taker_fee_rate": "0.006",
|
||
# "maker_fee_rate": "0.004"
|
||
# },
|
||
# "margin_rate": null,
|
||
# "goods_and_services_tax": null,
|
||
# "advanced_trade_only_volume": 0,
|
||
# "advanced_trade_only_fees": 0,
|
||
# "coinbase_pro_volume": 0,
|
||
# "coinbase_pro_fees": 0
|
||
# }
|
||
#
|
||
feeTier = self.safe_dict(fees, 'fee_tier', {})
|
||
expiringFeeTier = self.safe_dict(expiringFees, 'fee_tier', {}) # fee tier null?
|
||
perpetualFeeTier = self.safe_dict(perpetualFees, 'fee_tier', {}) # fee tier null?
|
||
data = self.safe_list(spot, 'products', [])
|
||
result = []
|
||
for i in range(0, len(data)):
|
||
result.append(self.parse_spot_market(data[i], feeTier))
|
||
futureData = self.safe_list(expiringFutures, 'products', [])
|
||
for i in range(0, len(futureData)):
|
||
result.append(self.parse_contract_market(futureData[i], expiringFeeTier))
|
||
perpetualData = self.safe_list(perpetualFutures, 'products', [])
|
||
for i in range(0, len(perpetualData)):
|
||
result.append(self.parse_contract_market(perpetualData[i], perpetualFeeTier))
|
||
newMarkets = []
|
||
for i in range(0, len(result)):
|
||
market = result[i]
|
||
info = self.safe_value(market, 'info', {})
|
||
realMarketIds = self.safe_list(info, 'alias_to', [])
|
||
length = len(realMarketIds)
|
||
if length > 0:
|
||
market['alias'] = realMarketIds[0]
|
||
else:
|
||
market['alias'] = None
|
||
newMarkets.append(market)
|
||
return newMarkets
|
||
|
||
def parse_spot_market(self, market, feeTier) -> MarketInterface:
|
||
#
|
||
# {
|
||
# "product_id": "TONE-USD",
|
||
# "price": "0.01523",
|
||
# "price_percentage_change_24h": "1.94109772423025",
|
||
# "volume_24h": "19773129",
|
||
# "volume_percentage_change_24h": "437.0170530929949",
|
||
# "base_increment": "1",
|
||
# "quote_increment": "0.00001",
|
||
# "quote_min_size": "1",
|
||
# "quote_max_size": "10000000",
|
||
# "base_min_size": "26.7187147229469674",
|
||
# "base_max_size": "267187147.2294696735908216",
|
||
# "base_name": "TE-FOOD",
|
||
# "quote_name": "US Dollar",
|
||
# "watched": False,
|
||
# "is_disabled": False,
|
||
# "new": False,
|
||
# "status": "online",
|
||
# "cancel_only": False,
|
||
# "limit_only": False,
|
||
# "post_only": False,
|
||
# "trading_disabled": False,
|
||
# "auction_mode": False,
|
||
# "product_type": "SPOT",
|
||
# "quote_currency_id": "USD",
|
||
# "base_currency_id": "TONE",
|
||
# "fcm_trading_session_details": null,
|
||
# "mid_market_price": ""
|
||
# }
|
||
#
|
||
id = self.safe_string(market, 'product_id')
|
||
baseId = self.safe_string(market, 'base_currency_id')
|
||
quoteId = self.safe_string(market, 'quote_currency_id')
|
||
base = self.safe_currency_code(baseId)
|
||
quote = self.safe_currency_code(quoteId)
|
||
marketType = self.safe_string_lower(market, 'product_type')
|
||
tradingDisabled = self.safe_bool(market, 'trading_disabled')
|
||
stablePairs = self.safe_list(self.options, 'stablePairs', [])
|
||
defaultTakerFee = self.safe_number(self.fees['trading'], 'taker')
|
||
defaultMakerFee = self.safe_number(self.fees['trading'], 'maker')
|
||
takerFee = 0.00001 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'taker_fee_rate', defaultTakerFee)
|
||
makerFee = 0.0 if self.in_array(id, stablePairs) else self.safe_number(feeTier, 'maker_fee_rate', defaultMakerFee)
|
||
return self.safe_market_structure({
|
||
'id': id,
|
||
'symbol': base + '/' + quote,
|
||
'base': base,
|
||
'quote': quote,
|
||
'settle': None,
|
||
'baseId': baseId,
|
||
'quoteId': quoteId,
|
||
'settleId': None,
|
||
'type': marketType,
|
||
'spot': (marketType == 'spot'),
|
||
'margin': None,
|
||
'swap': False,
|
||
'future': False,
|
||
'option': False,
|
||
'active': not tradingDisabled,
|
||
'contract': False,
|
||
'linear': None,
|
||
'inverse': None,
|
||
'taker': takerFee,
|
||
'maker': makerFee,
|
||
'contractSize': None,
|
||
'expiry': None,
|
||
'expiryDatetime': None,
|
||
'strike': None,
|
||
'optionType': None,
|
||
'precision': {
|
||
'amount': self.safe_number(market, 'base_increment'),
|
||
'price': self.safe_number_2(market, 'price_increment', 'quote_increment'),
|
||
},
|
||
'limits': {
|
||
'leverage': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'amount': {
|
||
'min': self.safe_number(market, 'base_min_size'),
|
||
'max': self.safe_number(market, 'base_max_size'),
|
||
},
|
||
'price': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'cost': {
|
||
'min': self.safe_number(market, 'quote_min_size'),
|
||
'max': self.safe_number(market, 'quote_max_size'),
|
||
},
|
||
},
|
||
'created': None,
|
||
'info': market,
|
||
})
|
||
|
||
def parse_contract_market(self, market, feeTier) -> MarketInterface:
|
||
# expiring
|
||
#
|
||
# {
|
||
# "product_id":"BIT-26APR24-CDE",
|
||
# "price":"71145",
|
||
# "price_percentage_change_24h":"-2.36722931247427",
|
||
# "volume_24h":"108549",
|
||
# "volume_percentage_change_24h":"155.78255337197794",
|
||
# "base_increment":"1",
|
||
# "quote_increment":"0.01",
|
||
# "quote_min_size":"0",
|
||
# "quote_max_size":"100000000",
|
||
# "base_min_size":"1",
|
||
# "base_max_size":"100000000",
|
||
# "base_name":"",
|
||
# "quote_name":"US Dollar",
|
||
# "watched":false,
|
||
# "is_disabled":false,
|
||
# "new":false,
|
||
# "status":"",
|
||
# "cancel_only":false,
|
||
# "limit_only":false,
|
||
# "post_only":false,
|
||
# "trading_disabled":false,
|
||
# "auction_mode":false,
|
||
# "product_type":"FUTURE",
|
||
# "quote_currency_id":"USD",
|
||
# "base_currency_id":"",
|
||
# "fcm_trading_session_details":{
|
||
# "is_session_open":true,
|
||
# "open_time":"2024-04-08T22:00:00Z",
|
||
# "close_time":"2024-04-09T21:00:00Z"
|
||
# },
|
||
# "mid_market_price":"71105",
|
||
# "alias":"",
|
||
# "alias_to":[
|
||
# ],
|
||
# "base_display_symbol":"",
|
||
# "quote_display_symbol":"USD",
|
||
# "view_only":false,
|
||
# "price_increment":"5",
|
||
# "display_name":"BTC 26 APR 24",
|
||
# "product_venue":"FCM",
|
||
# "future_product_details":{
|
||
# "venue":"cde",
|
||
# "contract_code":"BIT",
|
||
# "contract_expiry":"2024-04-26T15:00:00Z",
|
||
# "contract_size":"0.01",
|
||
# "contract_root_unit":"BTC",
|
||
# "group_description":"Nano Bitcoin Futures",
|
||
# "contract_expiry_timezone":"Europe/London",
|
||
# "group_short_description":"Nano BTC",
|
||
# "risk_managed_by":"MANAGED_BY_FCM",
|
||
# "contract_expiry_type":"EXPIRING",
|
||
# "contract_display_name":"BTC 26 APR 24"
|
||
# }
|
||
# }
|
||
#
|
||
# perpetual
|
||
#
|
||
# {
|
||
# "product_id":"ETH-PERP-INTX",
|
||
# "price":"3630.98",
|
||
# "price_percentage_change_24h":"0.65142426292038",
|
||
# "volume_24h":"114020.1501",
|
||
# "volume_percentage_change_24h":"63.33650787154869",
|
||
# "base_increment":"0.0001",
|
||
# "quote_increment":"0.01",
|
||
# "quote_min_size":"10",
|
||
# "quote_max_size":"50000000",
|
||
# "base_min_size":"0.0001",
|
||
# "base_max_size":"50000",
|
||
# "base_name":"",
|
||
# "quote_name":"USDC",
|
||
# "watched":false,
|
||
# "is_disabled":false,
|
||
# "new":false,
|
||
# "status":"",
|
||
# "cancel_only":false,
|
||
# "limit_only":false,
|
||
# "post_only":false,
|
||
# "trading_disabled":false,
|
||
# "auction_mode":false,
|
||
# "product_type":"FUTURE",
|
||
# "quote_currency_id":"USDC",
|
||
# "base_currency_id":"",
|
||
# "fcm_trading_session_details":null,
|
||
# "mid_market_price":"3630.975",
|
||
# "alias":"",
|
||
# "alias_to":[],
|
||
# "base_display_symbol":"",
|
||
# "quote_display_symbol":"USDC",
|
||
# "view_only":false,
|
||
# "price_increment":"0.01",
|
||
# "display_name":"ETH PERP",
|
||
# "product_venue":"INTX",
|
||
# "future_product_details":{
|
||
# "venue":"",
|
||
# "contract_code":"ETH",
|
||
# "contract_expiry":null,
|
||
# "contract_size":"1",
|
||
# "contract_root_unit":"ETH",
|
||
# "group_description":"",
|
||
# "contract_expiry_timezone":"",
|
||
# "group_short_description":"",
|
||
# "risk_managed_by":"MANAGED_BY_VENUE",
|
||
# "contract_expiry_type":"PERPETUAL",
|
||
# "perpetual_details":{
|
||
# "open_interest":"0",
|
||
# "funding_rate":"0.000016",
|
||
# "funding_time":"2024-04-09T09:00:00.000008Z",
|
||
# "max_leverage":"10"
|
||
# },
|
||
# "contract_display_name":"ETH PERPETUAL"
|
||
# }
|
||
# }
|
||
#
|
||
id = self.safe_string(market, 'product_id')
|
||
futureProductDetails = self.safe_dict(market, 'future_product_details', {})
|
||
contractExpiryType = self.safe_string(futureProductDetails, 'contract_expiry_type')
|
||
contractSize = self.safe_number(futureProductDetails, 'contract_size')
|
||
contractExpire = self.safe_string(futureProductDetails, 'contract_expiry')
|
||
expireTimestamp = self.parse8601(contractExpire)
|
||
expireDateTime = self.iso8601(expireTimestamp)
|
||
isSwap = (contractExpiryType == 'PERPETUAL')
|
||
baseId = self.safe_string(futureProductDetails, 'contract_root_unit')
|
||
quoteId = self.safe_string(market, 'quote_currency_id')
|
||
base = self.safe_currency_code(baseId)
|
||
quote = self.safe_currency_code(quoteId)
|
||
tradingDisabled = self.safe_bool(market, 'is_disabled')
|
||
symbol = base + '/' + quote
|
||
type = None
|
||
if isSwap:
|
||
type = 'swap'
|
||
symbol = symbol + ':' + quote
|
||
else:
|
||
type = 'future'
|
||
symbol = symbol + ':' + quote + '-' + self.yymmdd(expireTimestamp)
|
||
takerFeeRate = self.safe_number(feeTier, 'taker_fee_rate')
|
||
makerFeeRate = self.safe_number(feeTier, 'maker_fee_rate')
|
||
taker = takerFeeRate if takerFeeRate else self.parse_number('0.06')
|
||
maker = makerFeeRate if makerFeeRate else self.parse_number('0.04')
|
||
return self.safe_market_structure({
|
||
'id': id,
|
||
'symbol': symbol,
|
||
'base': base,
|
||
'quote': quote,
|
||
'settle': quote,
|
||
'baseId': baseId,
|
||
'quoteId': quoteId,
|
||
'settleId': quoteId,
|
||
'type': type,
|
||
'spot': False,
|
||
'margin': False,
|
||
'swap': isSwap,
|
||
'future': not isSwap,
|
||
'option': False,
|
||
'active': not tradingDisabled,
|
||
'contract': True,
|
||
'linear': True,
|
||
'inverse': False,
|
||
'taker': taker,
|
||
'maker': maker,
|
||
'contractSize': contractSize,
|
||
'expiry': expireTimestamp,
|
||
'expiryDatetime': expireDateTime,
|
||
'strike': None,
|
||
'optionType': None,
|
||
'precision': {
|
||
'amount': self.safe_number(market, 'base_increment'),
|
||
'price': self.safe_number_2(market, 'price_increment', 'quote_increment'),
|
||
},
|
||
'limits': {
|
||
'leverage': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'amount': {
|
||
'min': self.safe_number(market, 'base_min_size'),
|
||
'max': self.safe_number(market, 'base_max_size'),
|
||
},
|
||
'price': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
'cost': {
|
||
'min': self.safe_number(market, 'quote_min_size'),
|
||
'max': self.safe_number(market, 'quote_max_size'),
|
||
},
|
||
},
|
||
'created': None,
|
||
'info': market,
|
||
})
|
||
|
||
async def fetch_currencies_from_cache(self, params={}):
|
||
options = self.safe_dict(self.options, 'fetchCurrencies', {})
|
||
timestamp = self.safe_integer(options, 'timestamp')
|
||
expires = self.safe_integer(options, 'expires', 1000)
|
||
now = self.milliseconds()
|
||
if (timestamp is None) or ((now - timestamp) > expires):
|
||
promises = [
|
||
self.v2PublicGetCurrencies(params),
|
||
self.v2PublicGetCurrenciesCrypto(params),
|
||
]
|
||
promisesResult = await asyncio.gather(*promises)
|
||
fiatResponse = self.safe_dict(promisesResult, 0, {})
|
||
#
|
||
# [
|
||
# "data": {
|
||
# id: 'IMP',
|
||
# name: 'Isle of Man Pound',
|
||
# min_size: '0.01'
|
||
# },
|
||
# ...
|
||
# ]
|
||
#
|
||
cryptoResponse = self.safe_dict(promisesResult, 1, {})
|
||
#
|
||
# {
|
||
# asset_id: '9476e3be-b731-47fa-82be-347fabc573d9',
|
||
# code: 'AERO',
|
||
# name: 'Aerodrome Finance',
|
||
# color: '#0433FF',
|
||
# sort_index: '340',
|
||
# exponent: '8',
|
||
# type: 'crypto',
|
||
# address_regex: '^(?:0x)?[0-9a-fA-F]{40}$'
|
||
# }
|
||
#
|
||
fiatData = self.safe_list(fiatResponse, 'data', [])
|
||
cryptoData = self.safe_list(cryptoResponse, 'data', [])
|
||
exchangeRates = await self.v2PublicGetExchangeRates(params)
|
||
self.options['fetchCurrencies'] = self.extend(options, {
|
||
'currencies': self.array_concat(fiatData, cryptoData),
|
||
'exchangeRates': exchangeRates,
|
||
'timestamp': now,
|
||
})
|
||
return self.safe_dict(self.options, 'fetchCurrencies', {})
|
||
|
||
async def fetch_currencies(self, params={}) -> Currencies:
|
||
"""
|
||
fetches all available currencies on an exchange
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-currencies#get-fiat-currencies
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates
|
||
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:returns dict: an associative dictionary of currencies
|
||
"""
|
||
promises = [
|
||
self.v2PublicGetCurrencies(params),
|
||
self.v2PublicGetCurrenciesCrypto(params),
|
||
self.v2PublicGetExchangeRates(params),
|
||
]
|
||
promisesResult = await asyncio.gather(*promises)
|
||
fiatResponse = self.safe_dict(promisesResult, 0, {})
|
||
#
|
||
# [
|
||
# "data": [
|
||
# {
|
||
# id: 'IMP',
|
||
# name: 'Isle of Man Pound',
|
||
# min_size: '0.01'
|
||
# },
|
||
# ...
|
||
#
|
||
cryptoResponse = self.safe_dict(promisesResult, 1, {})
|
||
#
|
||
# [
|
||
# "data": [
|
||
# {
|
||
# asset_id: '9476e3be-b731-47fa-82be-347fabc573d9',
|
||
# code: 'AERO',
|
||
# name: 'Aerodrome Finance',
|
||
# color: '#0433FF',
|
||
# sort_index: '340',
|
||
# exponent: '8',
|
||
# type: 'crypto',
|
||
# address_regex: '^(?:0x)?[0-9a-fA-F]{40}$'
|
||
# },
|
||
# ...
|
||
#
|
||
ratesResponse = self.safe_dict(promisesResult, 2, {})
|
||
fiatData = self.safe_list(fiatResponse, 'data', [])
|
||
cryptoData = self.safe_list(cryptoResponse, 'data', [])
|
||
ratesData = self.safe_dict(ratesResponse, 'data', {})
|
||
rates = self.safe_dict(ratesData, 'rates', {})
|
||
ratesIds = list(rates.keys())
|
||
currencies = self.array_concat(fiatData, cryptoData)
|
||
result: dict = {}
|
||
networks: dict = {}
|
||
networksById: dict = {}
|
||
for i in range(0, len(currencies)):
|
||
currency = currencies[i]
|
||
assetId = self.safe_string(currency, 'asset_id')
|
||
id = self.safe_string_2(currency, 'id', 'code')
|
||
code = self.safe_currency_code(id)
|
||
name = self.safe_string(currency, 'name')
|
||
self.options['networks'][code] = name.lower()
|
||
self.options['networksById'][code] = name.lower()
|
||
type = 'crypto' if (assetId is not None) else 'fiat'
|
||
result[code] = self.safe_currency_structure({
|
||
'info': currency,
|
||
'id': id,
|
||
'code': code,
|
||
'type': type,
|
||
'name': name,
|
||
'active': True,
|
||
'deposit': None,
|
||
'withdraw': None,
|
||
'fee': None,
|
||
'precision': None,
|
||
'networks': {}, # todo
|
||
'limits': {
|
||
'amount': {
|
||
'min': self.safe_number(currency, 'min_size'),
|
||
'max': None,
|
||
},
|
||
'withdraw': {
|
||
'min': None,
|
||
'max': None,
|
||
},
|
||
},
|
||
})
|
||
if assetId is not None:
|
||
lowerCaseName = name.lower()
|
||
networks[code] = lowerCaseName
|
||
networksById[lowerCaseName] = code
|
||
# we have to add other currencies here( https://discord.com/channels/1220414409550336183/1220464770239430761/1372215891940479098 )
|
||
for i in range(0, len(ratesIds)):
|
||
currencyId = ratesIds[i]
|
||
code = self.safe_currency_code(currencyId)
|
||
if not (code in result):
|
||
result[code] = self.safe_currency_structure({
|
||
'info': {},
|
||
'id': currencyId,
|
||
'code': code,
|
||
'type': 'crypto',
|
||
'networks': {}, # todo
|
||
})
|
||
self.options['networks'] = self.extend(networks, self.options['networks'])
|
||
self.options['networksById'] = self.extend(networksById, self.options['networksById'])
|
||
return result
|
||
|
||
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/advanced-trade/reference/retailbrokerageapi_getproducts
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-exchange-rates#get-exchange-rates
|
||
|
||
: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
|
||
:param boolean [params.usePrivate]: use private endpoint for fetching tickers
|
||
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
||
"""
|
||
method = self.safe_string(self.options, 'fetchTickers', 'fetchTickersV3')
|
||
if method == 'fetchTickersV3':
|
||
return await self.fetch_tickers_v3(symbols, params)
|
||
return await self.fetch_tickers_v2(symbols, params)
|
||
|
||
async def fetch_tickers_v2(self, symbols: Strings = None, params={}) -> Tickers:
|
||
await self.load_markets()
|
||
symbols = self.market_symbols(symbols)
|
||
request: dict = {
|
||
# 'currency': 'USD',
|
||
}
|
||
response = await self.v2PublicGetExchangeRates(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "data":{
|
||
# "currency":"USD",
|
||
# "rates":{
|
||
# "AED":"3.6731",
|
||
# "AFN":"103.163942",
|
||
# "ALL":"106.973038",
|
||
# }
|
||
# }
|
||
# }
|
||
#
|
||
data = self.safe_dict(response, 'data', {})
|
||
rates = self.safe_dict(data, 'rates', {})
|
||
quoteId = self.safe_string(data, 'currency')
|
||
result: dict = {}
|
||
baseIds = list(rates.keys())
|
||
delimiter = '-'
|
||
for i in range(0, len(baseIds)):
|
||
baseId = baseIds[i]
|
||
marketId = baseId + delimiter + quoteId
|
||
market = self.safe_market(marketId, None, delimiter)
|
||
symbol = market['symbol']
|
||
result[symbol] = self.parse_ticker(rates[baseId], market)
|
||
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
||
|
||
async def fetch_tickers_v3(self, symbols: Strings = None, params={}) -> Tickers:
|
||
await self.load_markets()
|
||
symbols = self.market_symbols(symbols)
|
||
request: dict = {}
|
||
if symbols is not None:
|
||
request['product_ids'] = self.market_ids(symbols)
|
||
marketType = None
|
||
marketType, params = self.handle_market_type_and_params('fetchTickers', self.get_market_from_symbols(symbols), params, 'default')
|
||
if marketType is not None and marketType != 'default':
|
||
request['product_type'] = 'FUTURE' if (marketType == 'swap') else 'SPOT'
|
||
response = None
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchTickers', 'usePrivate', False)
|
||
if usePrivate:
|
||
response = await self.v3PrivateGetBrokerageProducts(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PublicGetBrokerageMarketProducts(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "products": [
|
||
# {
|
||
# "product_id": "TONE-USD",
|
||
# "price": "0.01523",
|
||
# "price_percentage_change_24h": "1.94109772423025",
|
||
# "volume_24h": "19773129",
|
||
# "volume_percentage_change_24h": "437.0170530929949",
|
||
# "base_increment": "1",
|
||
# "quote_increment": "0.00001",
|
||
# "quote_min_size": "1",
|
||
# "quote_max_size": "10000000",
|
||
# "base_min_size": "26.7187147229469674",
|
||
# "base_max_size": "267187147.2294696735908216",
|
||
# "base_name": "TE-FOOD",
|
||
# "quote_name": "US Dollar",
|
||
# "watched": False,
|
||
# "is_disabled": False,
|
||
# "new": False,
|
||
# "status": "online",
|
||
# "cancel_only": False,
|
||
# "limit_only": False,
|
||
# "post_only": False,
|
||
# "trading_disabled": False,
|
||
# "auction_mode": False,
|
||
# "product_type": "SPOT",
|
||
# "quote_currency_id": "USD",
|
||
# "base_currency_id": "TONE",
|
||
# "fcm_trading_session_details": null,
|
||
# "mid_market_price": ""
|
||
# },
|
||
# ...
|
||
# ],
|
||
# "num_products": 549
|
||
# }
|
||
#
|
||
data = self.safe_list(response, 'products', [])
|
||
result: dict = {}
|
||
for i in range(0, len(data)):
|
||
entry = data[i]
|
||
marketId = self.safe_string(entry, 'product_id')
|
||
market = self.safe_market(marketId, None, '-')
|
||
symbol = market['symbol']
|
||
result[symbol] = self.parse_ticker(entry, market)
|
||
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
||
|
||
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
||
"""
|
||
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getmarkettrades
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-spot-price
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-buy-price
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-prices#get-sell-price
|
||
|
||
:param str symbol: unified symbol of the market to fetch the ticker for
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.usePrivate]: whether to use the private endpoint for fetching the ticker
|
||
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
||
"""
|
||
method = self.safe_string(self.options, 'fetchTicker', 'fetchTickerV3')
|
||
if method == 'fetchTickerV3':
|
||
return await self.fetch_ticker_v3(symbol, params)
|
||
return await self.fetch_ticker_v2(symbol, params)
|
||
|
||
async def fetch_ticker_v2(self, symbol: str, params={}):
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
request = self.extend({
|
||
'symbol': market['id'],
|
||
}, params)
|
||
spot = await self.v2PublicGetPricesSymbolSpot(request)
|
||
#
|
||
# {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}}
|
||
#
|
||
ask = await self.v2PublicGetPricesSymbolBuy(request)
|
||
#
|
||
# {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}}
|
||
#
|
||
bid = await self.v2PublicGetPricesSymbolSell(request)
|
||
#
|
||
# {"data":{"base":"BTC","currency":"USD","amount":"48691.23"}}
|
||
#
|
||
spotData = self.safe_dict(spot, 'data', {})
|
||
askData = self.safe_dict(ask, 'data', {})
|
||
bidData = self.safe_dict(bid, 'data', {})
|
||
bidAskLast: dict = {
|
||
'bid': self.safe_number(bidData, 'amount'),
|
||
'ask': self.safe_number(askData, 'amount'),
|
||
'price': self.safe_number(spotData, 'amount'),
|
||
}
|
||
return self.parse_ticker(bidAskLast, market)
|
||
|
||
async def fetch_ticker_v3(self, symbol: str, params={}):
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'product_id': market['id'],
|
||
'limit': 1,
|
||
}
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchTicker', 'usePrivate', False)
|
||
response = None
|
||
if usePrivate:
|
||
response = await self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "trades": [
|
||
# {
|
||
# "trade_id": "518078013",
|
||
# "product_id": "BTC-USD",
|
||
# "price": "28208.1",
|
||
# "size": "0.00659179",
|
||
# "time": "2023-04-04T23:05:34.492746Z",
|
||
# "side": "BUY",
|
||
# "bid": "",
|
||
# "ask": ""
|
||
# }
|
||
# ],
|
||
# "best_bid": "28208.61",
|
||
# "best_ask": "28208.62"
|
||
# }
|
||
#
|
||
data = self.safe_list(response, 'trades', [])
|
||
ticker = self.parse_ticker(data[0], market)
|
||
ticker['bid'] = self.safe_number(response, 'best_bid')
|
||
ticker['ask'] = self.safe_number(response, 'best_ask')
|
||
return ticker
|
||
|
||
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
||
#
|
||
# fetchTickerV2
|
||
#
|
||
# {
|
||
# "bid": 20713.37,
|
||
# "ask": 20924.65,
|
||
# "price": 20809.83
|
||
# }
|
||
#
|
||
# fetchTickerV3
|
||
#
|
||
# {
|
||
# "trade_id": "10209805",
|
||
# "product_id": "BTC-USDT",
|
||
# "price": "19381.27",
|
||
# "size": "0.1",
|
||
# "time": "2023-01-13T20:35:41.865970Z",
|
||
# "side": "BUY",
|
||
# "bid": "",
|
||
# "ask": ""
|
||
# }
|
||
#
|
||
# fetchTickersV2
|
||
#
|
||
# "48691.23"
|
||
#
|
||
# fetchTickersV3
|
||
#
|
||
# [
|
||
# {
|
||
# "product_id": "ETH-USD",
|
||
# "price": "4471.59",
|
||
# "price_percentage_change_24h": "0.14243387238731",
|
||
# "volume_24h": "87329.92990204",
|
||
# "volume_percentage_change_24h": "-60.7789801794578",
|
||
# "base_increment": "0.00000001",
|
||
# "quote_increment": "0.01",
|
||
# "quote_min_size": "1",
|
||
# "quote_max_size": "150000000",
|
||
# "base_min_size": "0.00000001",
|
||
# "base_max_size": "42000",
|
||
# "base_name": "Ethereum",
|
||
# "quote_name": "US Dollar",
|
||
# "watched": False,
|
||
# "is_disabled": False,
|
||
# "new": False,
|
||
# "status": "online",
|
||
# "cancel_only": False,
|
||
# "limit_only": False,
|
||
# "post_only": False,
|
||
# "trading_disabled": False,
|
||
# "auction_mode": False,
|
||
# "product_type": "SPOT",
|
||
# "quote_currency_id": "USD",
|
||
# "base_currency_id": "ETH",
|
||
# "fcm_trading_session_details": null,
|
||
# "mid_market_price": "",
|
||
# "alias": "",
|
||
# "alias_to": ["ETH-USDC"],
|
||
# "base_display_symbol": "ETH",
|
||
# "quote_display_symbol": "USD",
|
||
# "view_only": False,
|
||
# "price_increment": "0.01",
|
||
# "display_name": "ETH-USD",
|
||
# "product_venue": "CBE",
|
||
# "approximate_quote_24h_volume": "390503641.25",
|
||
# "new_at": "2023-01-01T00:00:00Z"
|
||
# },
|
||
# ...
|
||
# ]
|
||
#
|
||
# fetchBidsAsks
|
||
#
|
||
# {
|
||
# "product_id": "TRAC-EUR",
|
||
# "bids": [
|
||
# {
|
||
# "price": "0.2384",
|
||
# "size": "386.1"
|
||
# }
|
||
# ],
|
||
# "asks": [
|
||
# {
|
||
# "price": "0.2406",
|
||
# "size": "672"
|
||
# }
|
||
# ],
|
||
# "time": "2023-06-30T07:15:24.656044Z"
|
||
# }
|
||
#
|
||
bid = self.safe_number(ticker, 'bid')
|
||
ask = self.safe_number(ticker, 'ask')
|
||
bidVolume = None
|
||
askVolume = None
|
||
if ('bids' in ticker):
|
||
bids = self.safe_list(ticker, 'bids', [])
|
||
asks = self.safe_list(ticker, 'asks', [])
|
||
firstBid = self.safe_dict(bids, 0, {})
|
||
firstAsk = self.safe_dict(asks, 0, {})
|
||
bid = self.safe_number(firstBid, 'price')
|
||
bidVolume = self.safe_number(firstBid, 'size')
|
||
ask = self.safe_number(firstAsk, 'price')
|
||
askVolume = self.safe_number(firstAsk, 'size')
|
||
marketId = self.safe_string(ticker, 'product_id')
|
||
market = self.safe_market(marketId, market)
|
||
last = self.safe_number(ticker, 'price')
|
||
datetime = self.safe_string(ticker, 'time')
|
||
return self.safe_ticker({
|
||
'symbol': market['symbol'],
|
||
'timestamp': self.parse8601(datetime),
|
||
'datetime': datetime,
|
||
'bid': bid,
|
||
'ask': ask,
|
||
'last': last,
|
||
'high': None,
|
||
'low': None,
|
||
'bidVolume': bidVolume,
|
||
'askVolume': askVolume,
|
||
'vwap': None,
|
||
'open': None,
|
||
'close': last,
|
||
'previousClose': None,
|
||
'change': None,
|
||
'percentage': self.safe_number(ticker, 'price_percentage_change_24h'),
|
||
'average': None,
|
||
'baseVolume': self.safe_number(ticker, 'volume_24h'),
|
||
'quoteVolume': self.safe_number(ticker, 'approximate_quote_24h_volume'),
|
||
'info': ticker,
|
||
}, market)
|
||
|
||
def parse_custom_balance(self, response, params={}):
|
||
balances = self.safe_list_2(response, 'data', 'accounts', [])
|
||
accounts = self.safe_list(params, 'type', self.options['accounts'])
|
||
v3Accounts = self.safe_list(params, 'type', self.options['v3Accounts'])
|
||
result: dict = {'info': response}
|
||
for b in range(0, len(balances)):
|
||
balance = balances[b]
|
||
type = self.safe_string(balance, 'type')
|
||
if self.in_array(type, accounts):
|
||
value = self.safe_dict(balance, 'balance')
|
||
if value is not None:
|
||
currencyId = self.safe_string(value, 'currency')
|
||
code = self.safe_currency_code(currencyId)
|
||
total = self.safe_string(value, 'amount')
|
||
free = total
|
||
account = self.safe_dict(result, code)
|
||
if account is None:
|
||
account = self.account()
|
||
account['free'] = free
|
||
account['total'] = total
|
||
else:
|
||
account['free'] = Precise.string_add(account['free'], total)
|
||
account['total'] = Precise.string_add(account['total'], total)
|
||
result[code] = account
|
||
elif self.in_array(type, v3Accounts):
|
||
available = self.safe_dict(balance, 'available_balance')
|
||
hold = self.safe_dict(balance, 'hold')
|
||
if available is not None and hold is not None:
|
||
currencyId = self.safe_string(available, 'currency')
|
||
code = self.safe_currency_code(currencyId)
|
||
used = self.safe_string(hold, 'value')
|
||
free = self.safe_string(available, 'value')
|
||
total = Precise.string_add(used, free)
|
||
account = self.safe_dict(result, code)
|
||
if account is None:
|
||
account = self.account()
|
||
account['free'] = free
|
||
account['used'] = used
|
||
account['total'] = total
|
||
else:
|
||
account['free'] = Precise.string_add(account['free'], free)
|
||
account['used'] = Precise.string_add(account['used'], used)
|
||
account['total'] = Precise.string_add(account['total'], total)
|
||
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/advanced-trade/reference/retailbrokerageapi_getaccounts
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-accounts#list-accounts
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmbalancesummary
|
||
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.v3]: default False, set True to use v3 api endpoint
|
||
:param str [params.type]: "spot"(default) or "swap" or "future"
|
||
:param int [params.limit]: default 250, maximum number of accounts to return
|
||
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
request: dict = {}
|
||
response = None
|
||
isV3 = self.safe_bool(params, 'v3', False)
|
||
params = self.omit(params, ['v3'])
|
||
marketType = None
|
||
marketType, params = self.handle_market_type_and_params('fetchBalance', None, params)
|
||
method = self.safe_string(self.options, 'fetchBalance', 'v3PrivateGetBrokerageAccounts')
|
||
if marketType == 'future':
|
||
response = await self.v3PrivateGetBrokerageCfmBalanceSummary(self.extend(request, params))
|
||
elif (isV3) or (method == 'v3PrivateGetBrokerageAccounts'):
|
||
request['limit'] = 250
|
||
response = await self.v3PrivateGetBrokerageAccounts(self.extend(request, params))
|
||
else:
|
||
request['limit'] = 250
|
||
response = await self.v2PrivateGetAccounts(self.extend(request, params))
|
||
#
|
||
# v2PrivateGetAccounts
|
||
# {
|
||
# "pagination":{
|
||
# "ending_before":null,
|
||
# "starting_after":null,
|
||
# "previous_ending_before":null,
|
||
# "next_starting_after":"6b17acd6-2e68-5eb0-9f45-72d67cef578a",
|
||
# "limit":100,
|
||
# "order":"desc",
|
||
# "previous_uri":null,
|
||
# "next_uri":"/v2/accounts?limit=100\u0026starting_after=6b17acd6-2e68-5eb0-9f45-72d67cef578b"
|
||
# },
|
||
# "data":[
|
||
# {
|
||
# "id":"94ad58bc-0f15-5309-b35a-a4c86d7bad60",
|
||
# "name":"MINA Wallet",
|
||
# "primary":false,
|
||
# "type":"wallet",
|
||
# "currency":{
|
||
# "code":"MINA",
|
||
# "name":"Mina",
|
||
# "color":"#EA6B48",
|
||
# "sort_index":397,
|
||
# "exponent":9,
|
||
# "type":"crypto",
|
||
# "address_regex":"^(B62)[A-Za-z0-9]{52}$",
|
||
# "asset_id":"a4ffc575-942c-5e26-b70c-cb3befdd4229",
|
||
# "slug":"mina"
|
||
# },
|
||
# "balance":{"amount":"0.000000000","currency":"MINA"},
|
||
# "created_at":"2022-03-25T00:36:16Z",
|
||
# "updated_at":"2022-03-25T00:36:16Z",
|
||
# "resource":"account",
|
||
# "resource_path":"/v2/accounts/94ad58bc-0f15-5309-b35a-a4c86d7bad60",
|
||
# "allow_deposits":true,
|
||
# "allow_withdrawals":true
|
||
# },
|
||
# ]
|
||
# }
|
||
#
|
||
# v3PrivateGetBrokerageAccounts
|
||
# {
|
||
# "accounts": [
|
||
# {
|
||
# "uuid": "11111111-1111-1111-1111-111111111111",
|
||
# "name": "USDC Wallet",
|
||
# "currency": "USDC",
|
||
# "available_balance": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "default": True,
|
||
# "active": True,
|
||
# "created_at": "2023-01-04T06:20:06.456Z",
|
||
# "updated_at": "2023-01-04T06:20:07.181Z",
|
||
# "deleted_at": null,
|
||
# "type": "ACCOUNT_TYPE_CRYPTO",
|
||
# "ready": False,
|
||
# "hold": {
|
||
# "value": "0.0000000000000000",
|
||
# "currency": "USDC"
|
||
# }
|
||
# },
|
||
# ...
|
||
# ],
|
||
# "has_next": False,
|
||
# "cursor": "",
|
||
# "size": 9
|
||
# }
|
||
#
|
||
params['type'] = marketType
|
||
return self.parse_custom_balance(response, params)
|
||
|
||
async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
|
||
"""
|
||
Fetch the history of changes, i.e. actions done by the user or operations that altered the balance. Will return staking rewards, and crypto deposits or withdrawals.
|
||
|
||
https://docs.cdp.coinbase.com/coinbase-app/docs/api-transactions#list-transactions
|
||
|
||
: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 boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
||
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
|
||
"""
|
||
await self.load_markets()
|
||
paginate = False
|
||
paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchLedger', code, since, limit, params, 'next_starting_after', 'starting_after', None, 100)
|
||
currency = None
|
||
if code is not None:
|
||
currency = self.currency(code)
|
||
request = None
|
||
request, params = await self.prepare_account_request_with_currency_code(code, limit, params)
|
||
# for pagination use parameter 'starting_after'
|
||
# the value for the next page can be obtained from the result of the previous call in the 'pagination' field
|
||
# eg: instance.last_http_response -> pagination.next_starting_after
|
||
response = await self.v2PrivateGetAccountsAccountIdTransactions(self.extend(request, params))
|
||
ledger = self.parse_ledger(response['data'], currency, since, limit)
|
||
length = len(ledger)
|
||
if length == 0:
|
||
return ledger
|
||
lastIndex = length - 1
|
||
last = self.safe_dict(ledger, lastIndex)
|
||
pagination = self.safe_dict(response, 'pagination', {})
|
||
cursor = self.safe_string(pagination, 'next_starting_after')
|
||
if (cursor is not None) and (cursor != ''):
|
||
last['info']['next_starting_after'] = cursor
|
||
ledger[lastIndex] = last
|
||
return ledger
|
||
|
||
def parse_ledger_entry_status(self, status):
|
||
types: dict = {
|
||
'completed': 'ok',
|
||
}
|
||
return self.safe_string(types, status, status)
|
||
|
||
def parse_ledger_entry_type(self, type):
|
||
types: dict = {
|
||
'buy': 'trade',
|
||
'sell': 'trade',
|
||
'fiat_deposit': 'transaction',
|
||
'fiat_withdrawal': 'transaction',
|
||
'exchange_deposit': 'transaction', # fiat withdrawal(from coinbase to coinbasepro)
|
||
'exchange_withdrawal': 'transaction', # fiat deposit(to coinbase from coinbasepro)
|
||
'send': 'transaction', # crypto deposit OR withdrawal
|
||
'pro_deposit': 'transaction', # crypto withdrawal(from coinbase to coinbasepro)
|
||
'pro_withdrawal': 'transaction', # crypto deposit(to coinbase from coinbasepro)
|
||
}
|
||
return self.safe_string(types, type, type)
|
||
|
||
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
||
#
|
||
# crypto deposit transaction
|
||
#
|
||
# {
|
||
# "id": "34e4816b-4c8c-5323-a01c-35a9fa26e490",
|
||
# "type": "send",
|
||
# "status": "completed",
|
||
# "amount": {amount: "28.31976528", currency: "BCH"},
|
||
# "native_amount": {amount: "2799.65", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2019-02-28T12:35:20Z",
|
||
# "updated_at": "2019-02-28T12:43:24Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/34e4816b-4c8c-5323-a01c-35a9fa26e490",
|
||
# "instant_exchange": False,
|
||
# "network": {
|
||
# "status": "confirmed",
|
||
# "hash": "56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701",
|
||
# "transaction_url": "https://bch.btc.com/56222d865dae83774fccb2efbd9829cf08c75c94ce135bfe4276f3fb46d49701"
|
||
# },
|
||
# "from": {resource: "bitcoin_cash_network", currency: "BCH"},
|
||
# "details": {title: 'Received Bitcoin Cash', subtitle: "From Bitcoin Cash address"}
|
||
# }
|
||
#
|
||
# crypto withdrawal transaction
|
||
#
|
||
# {
|
||
# "id": "459aad99-2c41-5698-ac71-b6b81a05196c",
|
||
# "type": "send",
|
||
# "status": "completed",
|
||
# "amount": {amount: "-0.36775642", currency: "BTC"},
|
||
# "native_amount": {amount: "-1111.65", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2019-03-20T08:37:07Z",
|
||
# "updated_at": "2019-03-20T08:49:33Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/459aad99-2c41-5698-ac71-b6b81a05196c",
|
||
# "instant_exchange": False,
|
||
# "network": {
|
||
# "status": "confirmed",
|
||
# "hash": "2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b",
|
||
# "transaction_url": "https://blockchain.info/tx/2732bbcf35c69217c47b36dce64933d103895277fe25738ffb9284092701e05b",
|
||
# "transaction_fee": {amount: "0.00000000", currency: "BTC"},
|
||
# "transaction_amount": {amount: "0.36775642", currency: "BTC"},
|
||
# "confirmations": 15682
|
||
# },
|
||
# "to": {
|
||
# "resource": "bitcoin_address",
|
||
# "address": "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX",
|
||
# "currency": "BTC",
|
||
# "address_info": {address: "1AHnhqbvbYx3rnZx8uC7NbFZaTe4tafFHX"}
|
||
# },
|
||
# "idem": "da0a2f14-a2af-4c5a-a37e-d4484caf582bsend",
|
||
# "application": {
|
||
# "id": "5756ab6e-836b-553b-8950-5e389451225d",
|
||
# "resource": "application",
|
||
# "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d"
|
||
# },
|
||
# "details": {title: 'Sent Bitcoin', subtitle: "To Bitcoin address"}
|
||
# }
|
||
#
|
||
# withdrawal transaction from coinbase to coinbasepro
|
||
#
|
||
# {
|
||
# "id": "5b1b9fb8-5007-5393-b923-02903b973fdc",
|
||
# "type": "pro_deposit",
|
||
# "status": "completed",
|
||
# "amount": {amount: "-0.00001111", currency: "BCH"},
|
||
# "native_amount": {amount: "0.00", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2019-02-28T13:31:58Z",
|
||
# "updated_at": "2019-02-28T13:31:58Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/5b1b9fb8-5007-5393-b923-02903b973fdc",
|
||
# "instant_exchange": False,
|
||
# "application": {
|
||
# "id": "5756ab6e-836b-553b-8950-5e389451225d",
|
||
# "resource": "application",
|
||
# "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d"
|
||
# },
|
||
# "details": {title: 'Transferred Bitcoin Cash', subtitle: "To Coinbase Pro"}
|
||
# }
|
||
#
|
||
# withdrawal transaction from coinbase to gdax
|
||
#
|
||
# {
|
||
# "id": "badb7313-a9d3-5c07-abd0-00f8b44199b1",
|
||
# "type": "exchange_deposit",
|
||
# "status": "completed",
|
||
# "amount": {amount: "-0.43704149", currency: "BCH"},
|
||
# "native_amount": {amount: "-51.90", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2019-03-19T10:30:40Z",
|
||
# "updated_at": "2019-03-19T10:30:40Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c01d7364-edd7-5f3a-bd1d-de53d4cbb25e/transactions/badb7313-a9d3-5c07-abd0-00f8b44199b1",
|
||
# "instant_exchange": False,
|
||
# "details": {title: 'Transferred Bitcoin Cash', subtitle: "To GDAX"}
|
||
# }
|
||
#
|
||
# deposit transaction from gdax to coinbase
|
||
#
|
||
# {
|
||
# "id": "9c4b642c-8688-58bf-8962-13cef64097de",
|
||
# "type": "exchange_withdrawal",
|
||
# "status": "completed",
|
||
# "amount": {amount: "0.57729420", currency: "BTC"},
|
||
# "native_amount": {amount: "4418.72", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2018-02-17T11:33:33Z",
|
||
# "updated_at": "2018-02-17T11:33:33Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/9c4b642c-8688-58bf-8962-13cef64097de",
|
||
# "instant_exchange": False,
|
||
# "details": {title: 'Transferred Bitcoin', subtitle: "From GDAX"}
|
||
# }
|
||
#
|
||
# deposit transaction from coinbasepro to coinbase
|
||
#
|
||
# {
|
||
# "id": "8d6dd0b9-3416-568a-889d-8f112fae9e81",
|
||
# "type": "pro_withdrawal",
|
||
# "status": "completed",
|
||
# "amount": {amount: "0.40555386", currency: "BTC"},
|
||
# "native_amount": {amount: "1140.27", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2019-03-04T19:41:58Z",
|
||
# "updated_at": "2019-03-04T19:41:58Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/8d6dd0b9-3416-568a-889d-8f112fae9e81",
|
||
# "instant_exchange": False,
|
||
# "application": {
|
||
# "id": "5756ab6e-836b-553b-8950-5e389451225d",
|
||
# "resource": "application",
|
||
# "resource_path": "/v2/applications/5756ab6e-836b-553b-8950-5e389451225d"
|
||
# },
|
||
# "details": {title: 'Transferred Bitcoin', subtitle: "From Coinbase Pro"}
|
||
# }
|
||
#
|
||
# sell trade
|
||
#
|
||
# {
|
||
# "id": "a9409207-df64-585b-97ab-a50780d2149e",
|
||
# "type": "sell",
|
||
# "status": "completed",
|
||
# "amount": {amount: "-9.09922880", currency: "BTC"},
|
||
# "native_amount": {amount: "-7285.73", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2017-03-27T15:38:34Z",
|
||
# "updated_at": "2017-03-27T15:38:34Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/transactions/a9409207-df64-585b-97ab-a50780d2149e",
|
||
# "instant_exchange": False,
|
||
# "sell": {
|
||
# "id": "e3550b4d-8ae6-5de3-95fe-1fb01ba83051",
|
||
# "resource": "sell",
|
||
# "resource_path": "/v2/accounts/c6afbd34-4bd0-501e-8616-4862c193cd84/sells/e3550b4d-8ae6-5de3-95fe-1fb01ba83051"
|
||
# },
|
||
# "details": {
|
||
# "title": "Sold Bitcoin",
|
||
# "subtitle": "Using EUR Wallet",
|
||
# "payment_method_name": "EUR Wallet"
|
||
# }
|
||
# }
|
||
#
|
||
# buy trade
|
||
#
|
||
# {
|
||
# "id": "63eeed67-9396-5912-86e9-73c4f10fe147",
|
||
# "type": "buy",
|
||
# "status": "completed",
|
||
# "amount": {amount: "2.39605772", currency: "ETH"},
|
||
# "native_amount": {amount: "98.31", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2017-03-27T09:07:56Z",
|
||
# "updated_at": "2017-03-27T09:07:57Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/transactions/63eeed67-9396-5912-86e9-73c4f10fe147",
|
||
# "instant_exchange": False,
|
||
# "buy": {
|
||
# "id": "20b25b36-76c6-5353-aa57-b06a29a39d82",
|
||
# "resource": "buy",
|
||
# "resource_path": "/v2/accounts/8902f85d-4a69-5d74-82fe-8e390201bda7/buys/20b25b36-76c6-5353-aa57-b06a29a39d82"
|
||
# },
|
||
# "details": {
|
||
# "title": "Bought Ethereum",
|
||
# "subtitle": "Using EUR Wallet",
|
||
# "payment_method_name": "EUR Wallet"
|
||
# }
|
||
# }
|
||
#
|
||
# fiat deposit transaction
|
||
#
|
||
# {
|
||
# "id": "04ed4113-3732-5b0c-af86-b1d2146977d0",
|
||
# "type": "fiat_deposit",
|
||
# "status": "completed",
|
||
# "amount": {amount: "114.02", currency: "EUR"},
|
||
# "native_amount": {amount: "97.23", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2017-02-09T07:01:21Z",
|
||
# "updated_at": "2017-02-09T07:01:22Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/04ed4113-3732-5b0c-af86-b1d2146977d0",
|
||
# "instant_exchange": False,
|
||
# "fiat_deposit": {
|
||
# "id": "f34c19f3-b730-5e3d-9f72-96520448677a",
|
||
# "resource": "fiat_deposit",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/deposits/f34c19f3-b730-5e3d-9f72-96520448677a"
|
||
# },
|
||
# "details": {
|
||
# "title": "Deposited funds",
|
||
# "subtitle": "From SEPA Transfer(GB47 BARC 20..., reference CBADVI)",
|
||
# "payment_method_name": "SEPA Transfer(GB47 BARC 20..., reference CBADVI)"
|
||
# }
|
||
# }
|
||
#
|
||
# fiat withdrawal transaction
|
||
#
|
||
# {
|
||
# "id": "957d98e2-f80e-5e2f-a28e-02945aa93079",
|
||
# "type": "fiat_withdrawal",
|
||
# "status": "completed",
|
||
# "amount": {amount: "-11000.00", currency: "EUR"},
|
||
# "native_amount": {amount: "-9698.22", currency: "GBP"},
|
||
# "description": null,
|
||
# "created_at": "2017-12-06T13:19:19Z",
|
||
# "updated_at": "2017-12-06T13:19:19Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/transactions/957d98e2-f80e-5e2f-a28e-02945aa93079",
|
||
# "instant_exchange": False,
|
||
# "fiat_withdrawal": {
|
||
# "id": "f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7",
|
||
# "resource": "fiat_withdrawal",
|
||
# "resource_path": "/v2/accounts/91cd2d36-3a91-55b6-a5d4-0124cf105483/withdrawals/f4bf1fd9-ab3b-5de7-906d-ed3e23f7a4e7"
|
||
# },
|
||
# "details": {
|
||
# "title": "Withdrew funds",
|
||
# "subtitle": "To HSBC BANK PLC(GB74 MIDL...)",
|
||
# "payment_method_name": "HSBC BANK PLC(GB74 MIDL...)"
|
||
# }
|
||
# }
|
||
#
|
||
amountInfo = self.safe_dict(item, 'amount', {})
|
||
amount = self.safe_string(amountInfo, 'amount')
|
||
direction = None
|
||
if Precise.string_lt(amount, '0'):
|
||
direction = 'out'
|
||
amount = Precise.string_neg(amount)
|
||
else:
|
||
direction = 'in'
|
||
currencyId = self.safe_string(amountInfo, 'currency')
|
||
code = self.safe_currency_code(currencyId, currency)
|
||
currency = self.safe_currency(currencyId, currency)
|
||
#
|
||
# the address and txid do not belong to the unified ledger structure
|
||
#
|
||
# address = None
|
||
# if item['to']:
|
||
# address = self.safe_string(item['to'], 'address')
|
||
# }
|
||
# txid = None
|
||
#
|
||
fee = None
|
||
networkInfo = self.safe_dict(item, 'network', {})
|
||
# txid = network['hash'] # txid does not belong to the unified ledger structure
|
||
feeInfo = self.safe_dict(networkInfo, 'transaction_fee')
|
||
if feeInfo is not None:
|
||
feeCurrencyId = self.safe_string(feeInfo, 'currency')
|
||
feeCurrencyCode = self.safe_currency_code(feeCurrencyId, currency)
|
||
feeAmount = self.safe_number(feeInfo, 'amount')
|
||
fee = {
|
||
'cost': feeAmount,
|
||
'currency': feeCurrencyCode,
|
||
}
|
||
timestamp = self.parse8601(self.safe_string(item, 'created_at'))
|
||
id = self.safe_string(item, 'id')
|
||
type = self.parse_ledger_entry_type(self.safe_string(item, 'type'))
|
||
status = self.parse_ledger_entry_status(self.safe_string(item, 'status'))
|
||
path = self.safe_string(item, 'resource_path')
|
||
accountId = None
|
||
if path is not None:
|
||
parts = path.split('/')
|
||
numParts = len(parts)
|
||
if numParts > 3:
|
||
accountId = parts[3]
|
||
return self.safe_ledger_entry({
|
||
'info': item,
|
||
'id': id,
|
||
'timestamp': timestamp,
|
||
'datetime': self.iso8601(timestamp),
|
||
'direction': direction,
|
||
'account': accountId,
|
||
'referenceId': None,
|
||
'referenceAccount': None,
|
||
'type': type,
|
||
'currency': code,
|
||
'amount': self.parse_number(amount),
|
||
'before': None,
|
||
'after': None,
|
||
'status': status,
|
||
'fee': fee,
|
||
}, currency)
|
||
|
||
async def find_account_id(self, code, params={}):
|
||
await self.load_markets()
|
||
await self.load_accounts(False, params)
|
||
for i in range(0, len(self.accounts)):
|
||
account = self.accounts[i]
|
||
if account['code'] == code:
|
||
return account['id']
|
||
return None
|
||
|
||
def prepare_account_request(self, limit: Int = None, params={}):
|
||
accountId = self.safe_string_2(params, 'account_id', 'accountId')
|
||
if accountId is None:
|
||
raise ArgumentsRequired(self.id + ' prepareAccountRequest() method requires an account_id(or accountId) parameter')
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
}
|
||
if limit is not None:
|
||
request['limit'] = limit
|
||
return request
|
||
|
||
async def prepare_account_request_with_currency_code(self, code: Str = None, limit: Int = None, params={}):
|
||
accountId = self.safe_string_2(params, 'account_id', 'accountId')
|
||
params = self.omit(params, ['account_id', 'accountId'])
|
||
if accountId is None:
|
||
if code is None:
|
||
raise ArgumentsRequired(self.id + ' prepareAccountRequestWithCurrencyCode() method requires an account_id(or accountId) parameter OR a currency code argument')
|
||
accountId = await self.find_account_id(code, params)
|
||
if accountId is None:
|
||
raise ExchangeError(self.id + ' prepareAccountRequestWithCurrencyCode() could not find account id for ' + code + '. You might try to generate the deposit address in the website for that coin first.')
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
}
|
||
if limit is not None:
|
||
request['limit'] = limit
|
||
return [request, params]
|
||
|
||
async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}):
|
||
"""
|
||
create a market buy order by providing the symbol and cost
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder
|
||
|
||
:param str symbol: unified symbol of the market to create an order in
|
||
:param float cost: how much you want to trade in units of the quote currency
|
||
: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)
|
||
if not market['spot']:
|
||
raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot orders only')
|
||
params['createMarketBuyOrderRequiresPrice'] = False
|
||
return await self.create_order(symbol, 'market', 'buy', cost, None, params)
|
||
|
||
async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
||
"""
|
||
create a trade order
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_postorder
|
||
|
||
: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 you want to trade in units of the base currency, quote currency for 'market' 'buy' orders
|
||
:param float [price]: the price to fulfill the order, in units of the quote currency, ignored in market orders
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param float [params.stopPrice]: price to trigger stop orders
|
||
:param float [params.triggerPrice]: price to trigger stop orders
|
||
:param float [params.stopLossPrice]: price to trigger stop-loss orders
|
||
:param float [params.takeProfitPrice]: price to trigger take-profit orders
|
||
:param bool [params.postOnly]: True or False
|
||
:param str [params.timeInForce]: 'GTC', 'IOC', 'GTD' or 'PO', 'FOK'
|
||
:param str [params.stop_direction]: 'UNKNOWN_STOP_DIRECTION', 'STOP_DIRECTION_STOP_UP', 'STOP_DIRECTION_STOP_DOWN' the direction the stopPrice is triggered from
|
||
:param str [params.end_time]: '2023-05-25T17:01:05.092Z' for 'GTD' orders
|
||
:param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount
|
||
:param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not
|
||
:param float [params.leverage]: default to 1, the leverage to use for the order
|
||
:param str [params.marginMode]: 'cross' or 'isolated'
|
||
:param str [params.retail_portfolio_id]: portfolio uid
|
||
:param boolean [params.is_max]: Used in conjunction with tradable_balance to indicate the user wants to use their entire tradable balance
|
||
:param str [params.tradable_balance]: amount of tradable balance
|
||
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
id = self.safe_string(self.options, 'brokerId', 'ccxt')
|
||
request: dict = {
|
||
'client_order_id': id + '-' + self.uuid(),
|
||
'product_id': market['id'],
|
||
'side': side.upper(),
|
||
}
|
||
triggerPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice'])
|
||
stopLossPrice = self.safe_number(params, 'stopLossPrice')
|
||
takeProfitPrice = self.safe_number(params, 'takeProfitPrice')
|
||
isStop = triggerPrice is not None
|
||
isStopLoss = stopLossPrice is not None
|
||
isTakeProfit = takeProfitPrice is not None
|
||
timeInForce = self.safe_string(params, 'timeInForce')
|
||
postOnly = True if (timeInForce == 'PO') else self.safe_bool_2(params, 'postOnly', 'post_only', False)
|
||
endTime = self.safe_string(params, 'end_time')
|
||
stopDirection = self.safe_string(params, 'stop_direction')
|
||
if type == 'limit':
|
||
if isStop:
|
||
if stopDirection is None:
|
||
stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP'
|
||
if (timeInForce == 'GTD') or (endTime is not None):
|
||
if endTime is None:
|
||
raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order')
|
||
request['order_configuration'] = {
|
||
'stop_limit_stop_limit_gtd': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
'stop_price': self.price_to_precision(symbol, triggerPrice),
|
||
'stop_direction': stopDirection,
|
||
'end_time': endTime,
|
||
},
|
||
}
|
||
else:
|
||
request['order_configuration'] = {
|
||
'stop_limit_stop_limit_gtc': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
'stop_price': self.price_to_precision(symbol, triggerPrice),
|
||
'stop_direction': stopDirection,
|
||
},
|
||
}
|
||
elif isStopLoss or isTakeProfit:
|
||
tpslPrice = None
|
||
if isStopLoss:
|
||
if stopDirection is None:
|
||
stopDirection = 'STOP_DIRECTION_STOP_UP' if (side == 'buy') else 'STOP_DIRECTION_STOP_DOWN'
|
||
tpslPrice = self.price_to_precision(symbol, stopLossPrice)
|
||
else:
|
||
if stopDirection is None:
|
||
stopDirection = 'STOP_DIRECTION_STOP_DOWN' if (side == 'buy') else 'STOP_DIRECTION_STOP_UP'
|
||
tpslPrice = self.price_to_precision(symbol, takeProfitPrice)
|
||
request['order_configuration'] = {
|
||
'stop_limit_stop_limit_gtc': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
'stop_price': tpslPrice,
|
||
'stop_direction': stopDirection,
|
||
},
|
||
}
|
||
else:
|
||
if (timeInForce == 'GTD') or (endTime is not None):
|
||
if endTime is None:
|
||
raise ExchangeError(self.id + ' createOrder() requires an end_time parameter for a GTD order')
|
||
request['order_configuration'] = {
|
||
'limit_limit_gtd': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
'end_time': endTime,
|
||
'post_only': postOnly,
|
||
},
|
||
}
|
||
elif timeInForce == 'IOC':
|
||
request['order_configuration'] = {
|
||
'sor_limit_ioc': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
},
|
||
}
|
||
elif timeInForce == 'FOK':
|
||
request['order_configuration'] = {
|
||
'limit_limit_fok': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
},
|
||
}
|
||
else:
|
||
request['order_configuration'] = {
|
||
'limit_limit_gtc': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
'limit_price': self.price_to_precision(symbol, price),
|
||
'post_only': postOnly,
|
||
},
|
||
}
|
||
else:
|
||
if isStop or isStopLoss or isTakeProfit:
|
||
raise NotSupported(self.id + ' createOrder() only stop limit orders are supported')
|
||
if market['spot'] and (side == 'buy'):
|
||
total = None
|
||
createMarketBuyOrderRequiresPrice = True
|
||
createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True)
|
||
cost = self.safe_number(params, 'cost')
|
||
params = self.omit(params, 'cost')
|
||
if cost is not None:
|
||
total = self.cost_to_precision(symbol, cost)
|
||
elif createMarketBuyOrderRequiresPrice:
|
||
if price is None:
|
||
raise InvalidOrder(self.id + ' createOrder() requires a price argument for market buy orders on spot markets to calculate the total amount to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend in the amount argument')
|
||
else:
|
||
amountString = self.number_to_string(amount)
|
||
priceString = self.number_to_string(price)
|
||
costRequest = Precise.string_mul(amountString, priceString)
|
||
total = self.cost_to_precision(symbol, costRequest)
|
||
else:
|
||
total = self.cost_to_precision(symbol, amount)
|
||
request['order_configuration'] = {
|
||
'market_market_ioc': {
|
||
'quote_size': total,
|
||
},
|
||
}
|
||
else:
|
||
request['order_configuration'] = {
|
||
'market_market_ioc': {
|
||
'base_size': self.amount_to_precision(symbol, amount),
|
||
},
|
||
}
|
||
marginMode = self.safe_string(params, 'marginMode')
|
||
if marginMode is not None:
|
||
if marginMode == 'isolated':
|
||
request['margin_type'] = 'ISOLATED'
|
||
elif marginMode == 'cross':
|
||
request['margin_type'] = 'CROSS'
|
||
params = self.omit(params, ['timeInForce', 'triggerPrice', 'stopLossPrice', 'takeProfitPrice', 'stopPrice', 'stop_price', 'stopDirection', 'stop_direction', 'clientOrderId', 'postOnly', 'post_only', 'end_time', 'marginMode'])
|
||
preview = self.safe_bool_2(params, 'preview', 'test', False)
|
||
response = None
|
||
if preview:
|
||
params = self.omit(params, ['preview', 'test'])
|
||
request = self.omit(request, 'client_order_id')
|
||
response = await self.v3PrivatePostBrokerageOrdersPreview(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PrivatePostBrokerageOrders(self.extend(request, params))
|
||
#
|
||
# successful order
|
||
#
|
||
# {
|
||
# "success": True,
|
||
# "failure_reason": "UNKNOWN_FAILURE_REASON",
|
||
# "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030",
|
||
# "success_response": {
|
||
# "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030",
|
||
# "product_id": "LTC-BTC",
|
||
# "side": "SELL",
|
||
# "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288"
|
||
# },
|
||
# "order_configuration": null
|
||
# }
|
||
#
|
||
# failed order
|
||
#
|
||
# {
|
||
# "success": False,
|
||
# "failure_reason": "UNKNOWN_FAILURE_REASON",
|
||
# "order_id": "",
|
||
# "error_response": {
|
||
# "error": "UNSUPPORTED_ORDER_CONFIGURATION",
|
||
# "message": "source is not enabled for trading",
|
||
# "error_details": "",
|
||
# "new_order_failure_reason": "UNSUPPORTED_ORDER_CONFIGURATION"
|
||
# },
|
||
# "order_configuration": {
|
||
# "limit_limit_gtc": {
|
||
# "base_size": "100",
|
||
# "limit_price": "40000",
|
||
# "post_only": False
|
||
# }
|
||
# }
|
||
# }
|
||
#
|
||
success = self.safe_bool(response, 'success')
|
||
if success is not True:
|
||
errorResponse = self.safe_dict(response, 'error_response')
|
||
errorTitle = self.safe_string(errorResponse, 'error')
|
||
errorMessage = self.safe_string(errorResponse, 'message')
|
||
if errorResponse is not None:
|
||
self.throw_exactly_matched_exception(self.exceptions['exact'], errorTitle, errorMessage)
|
||
self.throw_broadly_matched_exception(self.exceptions['broad'], errorTitle, errorMessage)
|
||
raise ExchangeError(errorMessage)
|
||
data = self.safe_dict(response, 'success_response', {})
|
||
return self.parse_order(data, market)
|
||
|
||
def parse_order(self, order: dict, market: Market = None) -> Order:
|
||
#
|
||
# createOrder
|
||
#
|
||
# {
|
||
# "order_id": "52cfe5e2-0b29-4c19-a245-a6a773de5030",
|
||
# "product_id": "LTC-BTC",
|
||
# "side": "SELL",
|
||
# "client_order_id": "4d760580-6fca-4094-a70b-ebcca8626288"
|
||
# }
|
||
#
|
||
# cancelOrder, cancelOrders
|
||
#
|
||
# {
|
||
# "success": True,
|
||
# "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON",
|
||
# "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d"
|
||
# }
|
||
#
|
||
# fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders
|
||
#
|
||
# {
|
||
# "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b",
|
||
# "product_id": "LTC-BTC",
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "order_configuration": {
|
||
# "limit_limit_gtc": {
|
||
# "base_size": "0.2",
|
||
# "limit_price": "0.006",
|
||
# "post_only": False
|
||
# },
|
||
# "stop_limit_stop_limit_gtc": {
|
||
# "base_size": "48.54",
|
||
# "limit_price": "6.998",
|
||
# "stop_price": "7.0687",
|
||
# "stop_direction": "STOP_DIRECTION_STOP_DOWN"
|
||
# }
|
||
# },
|
||
# "side": "SELL",
|
||
# "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c",
|
||
# "status": "OPEN",
|
||
# "time_in_force": "GOOD_UNTIL_CANCELLED",
|
||
# "created_time": "2023-01-16T23:37:23.947030Z",
|
||
# "completion_percentage": "0",
|
||
# "filled_size": "0",
|
||
# "average_filled_price": "0",
|
||
# "fee": "",
|
||
# "number_of_fills": "0",
|
||
# "filled_value": "0",
|
||
# "pending_cancel": False,
|
||
# "size_in_quote": False,
|
||
# "total_fees": "0",
|
||
# "size_inclusive_of_fees": False,
|
||
# "total_value_after_fees": "0",
|
||
# "trigger_status": "INVALID_ORDER_TYPE",
|
||
# "order_type": "LIMIT",
|
||
# "reject_reason": "REJECT_REASON_UNSPECIFIED",
|
||
# "settled": False,
|
||
# "product_type": "SPOT",
|
||
# "reject_message": "",
|
||
# "cancel_message": ""
|
||
# }
|
||
#
|
||
marketId = self.safe_string(order, 'product_id')
|
||
symbol = self.safe_symbol(marketId, market, '-')
|
||
if symbol is not None:
|
||
market = self.safe_market(symbol, market)
|
||
orderConfiguration = self.safe_dict(order, 'order_configuration', {})
|
||
limitGTC = self.safe_dict(orderConfiguration, 'limit_limit_gtc')
|
||
limitGTD = self.safe_dict(orderConfiguration, 'limit_limit_gtd')
|
||
limitIOC = self.safe_dict(orderConfiguration, 'sor_limit_ioc')
|
||
stopLimitGTC = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtc')
|
||
stopLimitGTD = self.safe_dict(orderConfiguration, 'stop_limit_stop_limit_gtd')
|
||
marketIOC = self.safe_dict(orderConfiguration, 'market_market_ioc')
|
||
isLimit = ((limitGTC is not None) or (limitGTD is not None) or (limitIOC is not None))
|
||
isStop = ((stopLimitGTC is not None) or (stopLimitGTD is not None))
|
||
price = None
|
||
amount = None
|
||
postOnly = None
|
||
triggerPrice = None
|
||
if isLimit:
|
||
target = None
|
||
if limitGTC is not None:
|
||
target = limitGTC
|
||
elif limitGTD is not None:
|
||
target = limitGTD
|
||
else:
|
||
target = limitIOC
|
||
price = self.safe_string(target, 'limit_price')
|
||
amount = self.safe_string(target, 'base_size')
|
||
postOnly = self.safe_bool(target, 'post_only')
|
||
elif isStop:
|
||
stopTarget = stopLimitGTC if (stopLimitGTC is not None) else stopLimitGTD
|
||
price = self.safe_string(stopTarget, 'limit_price')
|
||
amount = self.safe_string(stopTarget, 'base_size')
|
||
postOnly = self.safe_bool(stopTarget, 'post_only')
|
||
triggerPrice = self.safe_string(stopTarget, 'stop_price')
|
||
else:
|
||
amount = self.safe_string(marketIOC, 'base_size')
|
||
datetime = self.safe_string(order, 'created_time')
|
||
totalFees = self.safe_string(order, 'total_fees')
|
||
currencyFee = None
|
||
if (totalFees is not None) and (market is not None):
|
||
currencyFee = market['quote']
|
||
return self.safe_order({
|
||
'info': order,
|
||
'id': self.safe_string(order, 'order_id'),
|
||
'clientOrderId': self.safe_string(order, 'client_order_id'),
|
||
'timestamp': self.parse8601(datetime),
|
||
'datetime': datetime,
|
||
'lastTradeTimestamp': None,
|
||
'symbol': symbol,
|
||
'type': self.parse_order_type(self.safe_string(order, 'order_type')),
|
||
'timeInForce': self.parse_time_in_force(self.safe_string(order, 'time_in_force')),
|
||
'postOnly': postOnly,
|
||
'side': self.safe_string_lower(order, 'side'),
|
||
'price': price,
|
||
'triggerPrice': triggerPrice,
|
||
'amount': amount,
|
||
'filled': self.safe_string(order, 'filled_size'),
|
||
'remaining': None,
|
||
'cost': None,
|
||
'average': self.safe_string(order, 'average_filled_price'),
|
||
'status': self.parse_order_status(self.safe_string(order, 'status')),
|
||
'fee': {
|
||
'cost': self.safe_string(order, 'total_fees'),
|
||
'currency': currencyFee,
|
||
},
|
||
'trades': None,
|
||
}, market)
|
||
|
||
def parse_order_status(self, status: Str):
|
||
statuses: dict = {
|
||
'OPEN': 'open',
|
||
'FILLED': 'closed',
|
||
'CANCELLED': 'canceled',
|
||
'EXPIRED': 'canceled',
|
||
'FAILED': 'canceled',
|
||
'UNKNOWN_ORDER_STATUS': None,
|
||
}
|
||
return self.safe_string(statuses, status, status)
|
||
|
||
def parse_order_type(self, type: Str):
|
||
if type == 'UNKNOWN_ORDER_TYPE':
|
||
return None
|
||
types: dict = {
|
||
'MARKET': 'market',
|
||
'LIMIT': 'limit',
|
||
'STOP': 'limit',
|
||
'STOP_LIMIT': 'limit',
|
||
}
|
||
return self.safe_string(types, type, type)
|
||
|
||
def parse_time_in_force(self, timeInForce: Str):
|
||
timeInForces: dict = {
|
||
'GOOD_UNTIL_CANCELLED': 'GTC',
|
||
'GOOD_UNTIL_DATE_TIME': 'GTD',
|
||
'IMMEDIATE_OR_CANCEL': 'IOC',
|
||
'FILL_OR_KILL': 'FOK',
|
||
'UNKNOWN_TIME_IN_FORCE': None,
|
||
}
|
||
return self.safe_string(timeInForces, timeInForce, timeInForce)
|
||
|
||
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
||
"""
|
||
cancels an open order
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders
|
||
|
||
:param str id: order id
|
||
:param str symbol: not used by coinbase cancelOrder()
|
||
: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()
|
||
orders = await self.cancel_orders([id], symbol, params)
|
||
return self.safe_dict(orders, 0, {})
|
||
|
||
async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}):
|
||
"""
|
||
cancel multiple orders
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_cancelorders
|
||
|
||
:param str[] ids: order ids
|
||
:param str symbol: not used by coinbase cancelOrders()
|
||
: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()
|
||
market = None
|
||
if symbol is not None:
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'order_ids': ids,
|
||
}
|
||
response = await self.v3PrivatePostBrokerageOrdersBatchCancel(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "results": [
|
||
# {
|
||
# "success": True,
|
||
# "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON",
|
||
# "order_id": "bb8851a3-4fda-4a2c-aa06-9048db0e0f0d"
|
||
# }
|
||
# ]
|
||
# }
|
||
#
|
||
orders = self.safe_list(response, 'results', [])
|
||
for i in range(0, len(orders)):
|
||
success = self.safe_bool(orders[i], 'success')
|
||
if success is not True:
|
||
raise BadRequest(self.id + ' cancelOrders() has failed, check your arguments and parameters')
|
||
return self.parse_orders(orders, market)
|
||
|
||
async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
|
||
"""
|
||
edit a trade order
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_editorder
|
||
|
||
:param str id: cancel 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 currency you want to trade in units of base currency
|
||
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.preview]: default to False, wether to use the test/preview endpoint or not
|
||
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'order_id': id,
|
||
}
|
||
if amount is not None:
|
||
request['size'] = self.amount_to_precision(symbol, amount)
|
||
if price is not None:
|
||
request['price'] = self.price_to_precision(symbol, price)
|
||
preview = self.safe_bool_2(params, 'preview', 'test', False)
|
||
response = None
|
||
if preview:
|
||
params = self.omit(params, ['preview', 'test'])
|
||
response = await self.v3PrivatePostBrokerageOrdersEditPreview(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PrivatePostBrokerageOrdersEdit(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "success": True,
|
||
# "errors": {
|
||
# "edit_failure_reason": "UNKNOWN_EDIT_ORDER_FAILURE_REASON",
|
||
# "preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON"
|
||
# }
|
||
# }
|
||
#
|
||
return self.parse_order(response, market)
|
||
|
||
async def fetch_order(self, id: str, symbol: Str = None, params={}):
|
||
"""
|
||
fetches information on an order made by the user
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorder
|
||
|
||
:param str id: the order id
|
||
:param str symbol: unified market symbol that 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()
|
||
market = None
|
||
if symbol is not None:
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'order_id': id,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageOrdersHistoricalOrderId(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "order": {
|
||
# "order_id": "9bc1eb3b-5b46-4b71-9628-ae2ed0cca75b",
|
||
# "product_id": "LTC-BTC",
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "order_configuration": {
|
||
# "limit_limit_gtc": {
|
||
# "base_size": "0.2",
|
||
# "limit_price": "0.006",
|
||
# "post_only": False
|
||
# }
|
||
# },
|
||
# "side": "SELL",
|
||
# "client_order_id": "e5fe8482-05bb-428f-ad4d-dbc8ce39239c",
|
||
# "status": "OPEN",
|
||
# "time_in_force": "GOOD_UNTIL_CANCELLED",
|
||
# "created_time": "2023-01-16T23:37:23.947030Z",
|
||
# "completion_percentage": "0",
|
||
# "filled_size": "0",
|
||
# "average_filled_price": "0",
|
||
# "fee": "",
|
||
# "number_of_fills": "0",
|
||
# "filled_value": "0",
|
||
# "pending_cancel": False,
|
||
# "size_in_quote": False,
|
||
# "total_fees": "0",
|
||
# "size_inclusive_of_fees": False,
|
||
# "total_value_after_fees": "0",
|
||
# "trigger_status": "INVALID_ORDER_TYPE",
|
||
# "order_type": "LIMIT",
|
||
# "reject_reason": "REJECT_REASON_UNSPECIFIED",
|
||
# "settled": False,
|
||
# "product_type": "SPOT",
|
||
# "reject_message": "",
|
||
# "cancel_message": ""
|
||
# }
|
||
# }
|
||
#
|
||
order = self.safe_dict(response, 'order', {})
|
||
return self.parse_order(order, market)
|
||
|
||
async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = 100, params={}) -> List[Order]:
|
||
"""
|
||
fetches information on multiple orders made by the user
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders
|
||
|
||
:param str symbol: unified market symbol that the orders were made in
|
||
:param int [since]: the earliest time in ms to fetch orders
|
||
: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 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 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, 'fetchOrders', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 1000)
|
||
market = None
|
||
if symbol is not None:
|
||
market = self.market(symbol)
|
||
request: dict = {}
|
||
if market is not None:
|
||
request['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_integer_n(params, ['until'])
|
||
if until is not None:
|
||
params = self.omit(params, ['until'])
|
||
request['end_date'] = self.iso8601(until)
|
||
response = await self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "orders": [
|
||
# {
|
||
# "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8",
|
||
# "product_id": "BTC-USDT",
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "order_configuration": {
|
||
# "market_market_ioc": {
|
||
# "quote_size": "6.36"
|
||
# }
|
||
# },
|
||
# "side": "BUY",
|
||
# "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317",
|
||
# "status": "FILLED",
|
||
# "time_in_force": "IMMEDIATE_OR_CANCEL",
|
||
# "created_time": "2023-01-18T01:37:37.975552Z",
|
||
# "completion_percentage": "100",
|
||
# "filled_size": "0.000297920684505",
|
||
# "average_filled_price": "21220.6399999973697697",
|
||
# "fee": "",
|
||
# "number_of_fills": "2",
|
||
# "filled_value": "6.3220675944333996",
|
||
# "pending_cancel": False,
|
||
# "size_in_quote": True,
|
||
# "total_fees": "0.0379324055666004",
|
||
# "size_inclusive_of_fees": True,
|
||
# "total_value_after_fees": "6.36",
|
||
# "trigger_status": "INVALID_ORDER_TYPE",
|
||
# "order_type": "MARKET",
|
||
# "reject_reason": "REJECT_REASON_UNSPECIFIED",
|
||
# "settled": True,
|
||
# "product_type": "SPOT",
|
||
# "reject_message": "",
|
||
# "cancel_message": "Internal error"
|
||
# },
|
||
# ],
|
||
# "sequence": "0",
|
||
# "has_next": False,
|
||
# "cursor": ""
|
||
# }
|
||
#
|
||
orders = self.safe_list(response, 'orders', [])
|
||
first = self.safe_dict(orders, 0)
|
||
cursor = self.safe_string(response, 'cursor')
|
||
if (cursor is not None) and (cursor != ''):
|
||
first['cursor'] = cursor
|
||
orders[0] = first
|
||
return self.parse_orders(orders, market, since, limit)
|
||
|
||
async def fetch_orders_by_status(self, status, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
||
await self.load_markets()
|
||
market = None
|
||
if symbol is not None:
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'order_status': status,
|
||
}
|
||
if market is not None:
|
||
request['product_id'] = market['id']
|
||
if limit is None:
|
||
limit = 100
|
||
request['limit'] = limit
|
||
if since is not None:
|
||
request['start_date'] = self.iso8601(since)
|
||
until = self.safe_integer_n(params, ['until'])
|
||
if until is not None:
|
||
params = self.omit(params, ['until'])
|
||
request['end_date'] = self.iso8601(until)
|
||
response = await self.v3PrivateGetBrokerageOrdersHistoricalBatch(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "orders": [
|
||
# {
|
||
# "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8",
|
||
# "product_id": "BTC-USDT",
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "order_configuration": {
|
||
# "market_market_ioc": {
|
||
# "quote_size": "6.36"
|
||
# }
|
||
# },
|
||
# "side": "BUY",
|
||
# "client_order_id": "18eb9947-db49-4874-8e7b-39b8fe5f4317",
|
||
# "status": "FILLED",
|
||
# "time_in_force": "IMMEDIATE_OR_CANCEL",
|
||
# "created_time": "2023-01-18T01:37:37.975552Z",
|
||
# "completion_percentage": "100",
|
||
# "filled_size": "0.000297920684505",
|
||
# "average_filled_price": "21220.6399999973697697",
|
||
# "fee": "",
|
||
# "number_of_fills": "2",
|
||
# "filled_value": "6.3220675944333996",
|
||
# "pending_cancel": False,
|
||
# "size_in_quote": True,
|
||
# "total_fees": "0.0379324055666004",
|
||
# "size_inclusive_of_fees": True,
|
||
# "total_value_after_fees": "6.36",
|
||
# "trigger_status": "INVALID_ORDER_TYPE",
|
||
# "order_type": "MARKET",
|
||
# "reject_reason": "REJECT_REASON_UNSPECIFIED",
|
||
# "settled": True,
|
||
# "product_type": "SPOT",
|
||
# "reject_message": "",
|
||
# "cancel_message": "Internal error"
|
||
# },
|
||
# ],
|
||
# "sequence": "0",
|
||
# "has_next": False,
|
||
# "cursor": ""
|
||
# }
|
||
#
|
||
orders = self.safe_list(response, 'orders', [])
|
||
first = self.safe_dict(orders, 0)
|
||
cursor = self.safe_string(response, 'cursor')
|
||
if (cursor is not None) and (cursor != ''):
|
||
first['cursor'] = cursor
|
||
orders[0] = first
|
||
return self.parse_orders(orders, market, since, limit)
|
||
|
||
async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
||
"""
|
||
fetches information on all currently open orders
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders
|
||
|
||
:param str symbol: unified market symbol of the orders
|
||
:param int [since]: timestamp in ms of the earliest order, default is None
|
||
:param int [limit]: the maximum number of open order structures to retrieve
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
||
:param int [params.until]: the latest time in ms to fetch trades for
|
||
: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_cursor('fetchOpenOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100)
|
||
return await self.fetch_orders_by_status('OPEN', symbol, since, limit, params)
|
||
|
||
async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
||
"""
|
||
fetches information on multiple closed orders made by the user
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders
|
||
|
||
:param str symbol: unified market symbol of the orders
|
||
:param int [since]: timestamp in ms of the earliest order, default is None
|
||
:param int [limit]: the maximum number of closed order structures to retrieve
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
||
:param int [params.until]: the latest time in ms to fetch trades for
|
||
: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, 'fetchClosedOrders', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchClosedOrders', symbol, since, limit, params, 'cursor', 'cursor', None, 100)
|
||
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={}):
|
||
"""
|
||
fetches information on multiple canceled orders made by the user
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_gethistoricalorders
|
||
|
||
:param str symbol: unified market symbol of the orders
|
||
:param int [since]: timestamp in ms of the earliest order, default is None
|
||
:param int [limit]: the maximum number of canceled order structures to retrieve
|
||
: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>`
|
||
"""
|
||
return await self.fetch_orders_by_status('CANCELLED', symbol, since, limit, params)
|
||
|
||
async def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
||
"""
|
||
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpubliccandles
|
||
|
||
: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, not used by coinbase
|
||
: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)
|
||
:param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the candles
|
||
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
||
"""
|
||
await self.load_markets()
|
||
maxLimit = 300
|
||
limit = maxLimit if (limit is None) else min(limit, maxLimit)
|
||
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, maxLimit - 1)
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'product_id': market['id'],
|
||
'granularity': self.safe_string(self.timeframes, timeframe, timeframe),
|
||
}
|
||
until = self.safe_integer_n(params, ['until', 'end'])
|
||
params = self.omit(params, ['until'])
|
||
duration = self.parse_timeframe(timeframe)
|
||
requestedDuration = limit * duration
|
||
sinceString = None
|
||
if since is not None:
|
||
sinceString = self.number_to_string(self.parse_to_int(since / 1000))
|
||
else:
|
||
now = str(self.seconds())
|
||
sinceString = Precise.string_sub(now, str(requestedDuration))
|
||
request['start'] = sinceString
|
||
if until is not None:
|
||
request['end'] = self.number_to_string(self.parse_to_int(until / 1000))
|
||
else:
|
||
# 300 candles max
|
||
request['end'] = Precise.string_add(sinceString, str(requestedDuration))
|
||
response = None
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'usePrivate', False)
|
||
if usePrivate:
|
||
response = await self.v3PrivateGetBrokerageProductsProductIdCandles(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PublicGetBrokerageMarketProductsProductIdCandles(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "candles": [
|
||
# {
|
||
# "start": "1673391780",
|
||
# "low": "17414.36",
|
||
# "high": "17417.99",
|
||
# "open": "17417.74",
|
||
# "close": "17417.38",
|
||
# "volume": "1.87780853"
|
||
# },
|
||
# ]
|
||
# }
|
||
#
|
||
candles = self.safe_list(response, 'candles', [])
|
||
return self.parse_ohlcvs(candles, market, timeframe, since, limit)
|
||
|
||
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
||
#
|
||
# [
|
||
# {
|
||
# "start": "1673391780",
|
||
# "low": "17414.36",
|
||
# "high": "17417.99",
|
||
# "open": "17417.74",
|
||
# "close": "17417.38",
|
||
# "volume": "1.87780853"
|
||
# },
|
||
# ]
|
||
#
|
||
return [
|
||
self.safe_timestamp(ohlcv, 'start'),
|
||
self.safe_number(ohlcv, 'open'),
|
||
self.safe_number(ohlcv, 'high'),
|
||
self.safe_number(ohlcv, 'low'),
|
||
self.safe_number(ohlcv, 'close'),
|
||
self.safe_number(ohlcv, 'volume'),
|
||
]
|
||
|
||
async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
||
"""
|
||
get the list of most recent trades for a particular symbol
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicmarkettrades
|
||
|
||
:param str symbol: unified market symbol of the trades
|
||
:param int [since]: not used by coinbase fetchTrades
|
||
:param int [limit]: the maximum number of trade structures to fetch
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the trades
|
||
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
||
"""
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
request: dict = {
|
||
'product_id': market['id'],
|
||
}
|
||
if since is not None:
|
||
request['start'] = self.number_to_string(self.parse_to_int(since / 1000))
|
||
if limit is not None:
|
||
request['limit'] = min(limit, 1000)
|
||
until = None
|
||
until, params = self.handle_option_and_params(params, 'fetchTrades', 'until')
|
||
if until is not None:
|
||
request['end'] = self.number_to_string(self.parse_to_int(until / 1000))
|
||
elif since is not None:
|
||
raise ArgumentsRequired(self.id + ' fetchTrades() requires a `until` parameter when you use `since` argument')
|
||
response = None
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchTrades', 'usePrivate', False)
|
||
if usePrivate:
|
||
response = await self.v3PrivateGetBrokerageProductsProductIdTicker(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PublicGetBrokerageMarketProductsProductIdTicker(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "trades": [
|
||
# {
|
||
# "trade_id": "10092327",
|
||
# "product_id": "BTC-USDT",
|
||
# "price": "17488.12",
|
||
# "size": "0.0000623",
|
||
# "time": "2023-01-11T00:52:37.557001Z",
|
||
# "side": "BUY",
|
||
# "bid": "",
|
||
# "ask": ""
|
||
# },
|
||
# ]
|
||
# }
|
||
#
|
||
trades = self.safe_list(response, 'trades', [])
|
||
return self.parse_trades(trades, market, since, limit)
|
||
|
||
async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
||
"""
|
||
fetch all trades made by the user
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfills
|
||
|
||
:param str symbol: unified market symbol of the trades
|
||
:param int [since]: timestamp in ms of the earliest order, default is None
|
||
:param int [limit]: the maximum number of trade structures 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 Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
paginate = False
|
||
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
||
if paginate:
|
||
return await self.fetch_paginated_call_cursor('fetchMyTrades', symbol, since, limit, params, 'cursor', 'cursor', None, 250)
|
||
market = None
|
||
if symbol is not None:
|
||
market = self.market(symbol)
|
||
request: dict = {}
|
||
if market is not None:
|
||
request['product_id'] = market['id']
|
||
if limit is not None:
|
||
request['limit'] = limit
|
||
if since is not None:
|
||
request['start_sequence_timestamp'] = self.iso8601(since)
|
||
until = self.safe_integer_n(params, ['until'])
|
||
if until is not None:
|
||
params = self.omit(params, ['until'])
|
||
request['end_sequence_timestamp'] = self.iso8601(until)
|
||
response = await self.v3PrivateGetBrokerageOrdersHistoricalFills(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "fills": [
|
||
# {
|
||
# "entry_id": "b88b82cc89e326a2778874795102cbafd08dd979a2a7a3c69603fc4c23c2e010",
|
||
# "trade_id": "cdc39e45-bbd3-44ec-bf02-61742dfb16a1",
|
||
# "order_id": "813a53c5-3e39-47bb-863d-2faf685d22d8",
|
||
# "trade_time": "2023-01-18T01:37:38.091377090Z",
|
||
# "trade_type": "FILL",
|
||
# "price": "21220.64",
|
||
# "size": "0.0046830664333996",
|
||
# "commission": "0.0000280983986004",
|
||
# "product_id": "BTC-USDT",
|
||
# "sequence_timestamp": "2023-01-18T01:37:38.092520Z",
|
||
# "liquidity_indicator": "UNKNOWN_LIQUIDITY_INDICATOR",
|
||
# "size_in_quote": True,
|
||
# "user_id": "1111111-1111-1111-1111-111111111111",
|
||
# "side": "BUY"
|
||
# },
|
||
# ],
|
||
# "cursor": ""
|
||
# }
|
||
#
|
||
trades = self.safe_list(response, 'fills', [])
|
||
first = self.safe_dict(trades, 0)
|
||
cursor = self.safe_string(response, 'cursor')
|
||
if (cursor is not None) and (cursor != ''):
|
||
first['cursor'] = cursor
|
||
trades[0] = first
|
||
return self.parse_trades(trades, market, since, limit)
|
||
|
||
async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
||
"""
|
||
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpublicproductbook
|
||
|
||
: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
|
||
:param boolean [params.usePrivate]: default False, when True will use the private endpoint to fetch the order book
|
||
: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)
|
||
request: dict = {
|
||
'product_id': market['id'],
|
||
}
|
||
if limit is not None:
|
||
request['limit'] = limit
|
||
response = None
|
||
usePrivate = False
|
||
usePrivate, params = self.handle_option_and_params(params, 'fetchOrderBook', 'usePrivate', False)
|
||
if usePrivate:
|
||
response = await self.v3PrivateGetBrokerageProductBook(self.extend(request, params))
|
||
else:
|
||
response = await self.v3PublicGetBrokerageMarketProductBook(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "pricebook": {
|
||
# "product_id": "BTC-USDT",
|
||
# "bids": [
|
||
# {
|
||
# "price": "30757.85",
|
||
# "size": "0.115"
|
||
# },
|
||
# ],
|
||
# "asks": [
|
||
# {
|
||
# "price": "30759.07",
|
||
# "size": "0.04877659"
|
||
# },
|
||
# ],
|
||
# "time": "2023-06-30T04:02:40.533606Z"
|
||
# }
|
||
# }
|
||
#
|
||
data = self.safe_dict(response, 'pricebook', {})
|
||
time = self.safe_string(data, 'time')
|
||
timestamp = self.parse8601(time)
|
||
return self.parse_order_book(data, symbol, timestamp, 'bids', 'asks', 'price', 'size')
|
||
|
||
async def fetch_bids_asks(self, symbols: Strings = None, params={}):
|
||
"""
|
||
fetches the bid and ask price and volume for multiple markets
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getbestbidask
|
||
|
||
:param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets 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 = {}
|
||
if symbols is not None:
|
||
request['product_ids'] = self.market_ids(symbols)
|
||
response = await self.v3PrivateGetBrokerageBestBidAsk(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "pricebooks": [
|
||
# {
|
||
# "product_id": "TRAC-EUR",
|
||
# "bids": [
|
||
# {
|
||
# "price": "0.2384",
|
||
# "size": "386.1"
|
||
# }
|
||
# ],
|
||
# "asks": [
|
||
# {
|
||
# "price": "0.2406",
|
||
# "size": "672"
|
||
# }
|
||
# ],
|
||
# "time": "2023-06-30T07:15:24.656044Z"
|
||
# },
|
||
# ]
|
||
# }
|
||
#
|
||
tickers = self.safe_list(response, 'pricebooks', [])
|
||
return self.parse_tickers(tickers, symbols)
|
||
|
||
async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
||
"""
|
||
make a withdrawal
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-transactions#send-money
|
||
|
||
:param str code: unified currency code
|
||
:param float amount: the amount to withdraw
|
||
:param str address: the address to withdraw to
|
||
:param str [tag]: an optional tag for the withdrawal
|
||
: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)
|
||
accountId = self.safe_string_2(params, 'account_id', 'accountId')
|
||
params = self.omit(params, ['account_id', 'accountId'])
|
||
if accountId is None:
|
||
if code is None:
|
||
raise ArgumentsRequired(self.id + ' withdraw() requires an account_id(or accountId) parameter OR a currency code argument')
|
||
accountId = await self.find_account_id(code, params)
|
||
if accountId is None:
|
||
raise ExchangeError(self.id + ' withdraw() could not find account id for ' + code)
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
'type': 'send',
|
||
'to': address,
|
||
'amount': amount,
|
||
'currency': currency['id'],
|
||
}
|
||
if tag is not None:
|
||
request['destination_tag'] = tag
|
||
response = await self.v2PrivatePostAccountsAccountIdTransactions(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "data": {
|
||
# "id": "a1794ecf-5693-55fa-70cf-ef731748ed82",
|
||
# "type": "send",
|
||
# "status": "pending",
|
||
# "amount": {
|
||
# "amount": "-14.008308",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "native_amount": {
|
||
# "amount": "-18.74",
|
||
# "currency": "CAD"
|
||
# },
|
||
# "description": null,
|
||
# "created_at": "2024-01-12T01:27:31Z",
|
||
# "updated_at": "2024-01-12T01:27:31Z",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/a34bgfad-ed67-538b-bffc-730c98c10da0/transactions/a1794ecf-5693-55fa-70cf-ef731748ed82",
|
||
# "instant_exchange": False,
|
||
# "network": {
|
||
# "status": "pending",
|
||
# "status_description": "Pending(est. less than 10 minutes)",
|
||
# "transaction_fee": {
|
||
# "amount": "4.008308",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "transaction_amount": {
|
||
# "amount": "10.000000",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "confirmations": 0
|
||
# },
|
||
# "to": {
|
||
# "resource": "ethereum_address",
|
||
# "address": "0x9...",
|
||
# "currency": "USDC",
|
||
# "address_info": {
|
||
# "address": "0x9..."
|
||
# }
|
||
# },
|
||
# "idem": "748d8591-dg9a-7831-a45b-crd61dg78762",
|
||
# "details": {
|
||
# "title": "Sent USDC",
|
||
# "subtitle": "To USDC address on Ethereum network",
|
||
# "header": "Sent 14.008308 USDC($18.74)",
|
||
# "health": "warning"
|
||
# },
|
||
# "hide_native_amount": False
|
||
# }
|
||
# }
|
||
#
|
||
data = self.safe_dict(response, 'data', {})
|
||
return self.parse_transaction(data, currency)
|
||
|
||
async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]:
|
||
"""
|
||
fetch the deposit address for a currency associated with self account
|
||
|
||
https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses
|
||
|
||
:param str code: unified currency code
|
||
: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)
|
||
request = None
|
||
request, params = await self.prepare_account_request_with_currency_code(currency['code'], None, params)
|
||
response = await self.v2PrivateGetAccountsAccountIdAddresses(self.extend(request, params))
|
||
#
|
||
# {
|
||
# pagination: {
|
||
# ending_before: null,
|
||
# starting_after: null,
|
||
# previous_ending_before: null,
|
||
# next_starting_after: null,
|
||
# limit: '25',
|
||
# order: 'desc',
|
||
# previous_uri: null,
|
||
# next_uri: null
|
||
# },
|
||
# data: [
|
||
# {
|
||
# id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d',
|
||
# address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk',
|
||
# address_info: {address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk'},
|
||
# name: null,
|
||
# created_at: '2023-05-29T21:12:12Z',
|
||
# updated_at: '2023-05-29T21:12:12Z',
|
||
# network: 'solana',
|
||
# uri_scheme: 'solana',
|
||
# resource: 'address',
|
||
# resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d',
|
||
# warnings: [
|
||
# {
|
||
# type: 'correct_address_warning',
|
||
# title: 'This is an ERC20 USDC address.',
|
||
# details: 'Only send ERC20 USD Coin(USDC) to self address.',
|
||
# image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png',
|
||
# options: [{text: 'I understand', style: 'primary', id: 'dismiss'}]
|
||
# }
|
||
# ],
|
||
# qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png',
|
||
# address_label: 'USDC address(Solana)',
|
||
# default_receive: True,
|
||
# deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||
# callback_url: null,
|
||
# share_address_copy: {
|
||
# line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk',
|
||
# line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.'
|
||
# },
|
||
# receive_subtitle: 'ERC-20',
|
||
# inline_warning: {
|
||
# text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.',
|
||
# tooltip: {
|
||
# title: 'USDC(Solana)',
|
||
# subtitle: 'This address can only receive USDC-SPL from Solana network.'
|
||
# }
|
||
# }
|
||
# },
|
||
# ...
|
||
# ]
|
||
# }
|
||
#
|
||
data = self.safe_list(response, 'data', [])
|
||
addressStructures = self.parse_deposit_addresses(data, None, False)
|
||
return self.index_by(addressStructures, 'network')
|
||
|
||
def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress:
|
||
#
|
||
# {
|
||
# id: '64ceb5f1-5fa2-5310-a4ff-9fd46271003d',
|
||
# address: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk',
|
||
# address_info: {
|
||
# address: 'GCF74576I7AQ56SLMKBQAP255EGUOWCRVII3S44KEXVNJEOIFVBDMXVL',
|
||
# destination_tag: '3722061866'
|
||
# },
|
||
# name: null,
|
||
# created_at: '2023-05-29T21:12:12Z',
|
||
# updated_at: '2023-05-29T21:12:12Z',
|
||
# network: 'solana',
|
||
# uri_scheme: 'solana',
|
||
# resource: 'address',
|
||
# resource_path: '/v2/accounts/a7b3d387-bfb8-5ce7-b8da-1f507e81cf25/addresses/64ceb5f1-5fa2-5310-a4ff-9fd46271003d',
|
||
# warnings: [
|
||
# {
|
||
# type: 'correct_address_warning',
|
||
# title: 'This is an ERC20 USDC address.',
|
||
# details: 'Only send ERC20 USD Coin(USDC) to self address.',
|
||
# image_url: 'https://www.coinbase.com/assets/addresses/global-receive-warning-a3d91807e61c717e5a38d270965003dcc025ca8a3cea40ec3d7835b7c86087fa.png',
|
||
# options: [{text: 'I understand', style: 'primary', id: 'dismiss'}]
|
||
# }
|
||
# ],
|
||
# qr_code_image_url: 'https://static-assets.coinbase.com/p2p/l2/asset_network_combinations/v5/usdc-solana.png',
|
||
# address_label: 'USDC address(Solana)',
|
||
# default_receive: True,
|
||
# deposit_uri: 'solana:5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk?spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||
# callback_url: null,
|
||
# share_address_copy: {
|
||
# line1: '5xjPKeAXpnhA2kHyinvdVeui6RXVdEa3B2J3SCAwiKnk',
|
||
# line2: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.'
|
||
# },
|
||
# receive_subtitle: 'ERC-20',
|
||
# inline_warning: {
|
||
# text: 'This address can only receive USDC-SPL from Solana network. Don’t send USDC from other networks, other SPL tokens or NFTs, or it may result in a loss of funds.',
|
||
# tooltip: {
|
||
# title: 'USDC(Solana)',
|
||
# subtitle: 'This address can only receive USDC-SPL from Solana network.'
|
||
# }
|
||
# }
|
||
# }
|
||
#
|
||
address = self.safe_string(depositAddress, 'address')
|
||
self.check_address(address)
|
||
networkId = self.safe_string(depositAddress, 'network')
|
||
code = self.safe_currency_code(None, currency)
|
||
addressLabel = self.safe_string(depositAddress, 'address_label')
|
||
currencyId = None
|
||
if addressLabel is not None:
|
||
splitAddressLabel = addressLabel.split(' ')
|
||
currencyId = self.safe_string(splitAddressLabel, 0)
|
||
addressInfo = self.safe_dict(depositAddress, 'address_info')
|
||
return {
|
||
'info': depositAddress,
|
||
'currency': self.safe_currency_code(currencyId, currency),
|
||
'network': self.network_id_to_code(networkId, code),
|
||
'address': address,
|
||
'tag': self.safe_string(addressInfo, 'destination_tag'),
|
||
}
|
||
|
||
async def deposit(self, code: str, amount: float, id: str, params={}):
|
||
"""
|
||
make a deposit
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#deposit-funds
|
||
|
||
:param str code: unified currency code
|
||
:param float amount: the amount to deposit
|
||
:param str id: the payment method id to be used for the deposit, can be retrieved from v2PrivateGetPaymentMethods
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.accountId]: the id of the account to deposit into
|
||
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
accountId = self.safe_string_2(params, 'account_id', 'accountId')
|
||
params = self.omit(params, ['account_id', 'accountId'])
|
||
if accountId is None:
|
||
if code is None:
|
||
raise ArgumentsRequired(self.id + ' deposit() requires an account_id(or accountId) parameter OR a currency code argument')
|
||
accountId = await self.find_account_id(code, params)
|
||
if accountId is None:
|
||
raise ExchangeError(self.id + ' deposit() could not find account id for ' + code)
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
'amount': self.number_to_string(amount),
|
||
'currency': code.upper(), # need to use code in case depositing USD etc.
|
||
'payment_method': id,
|
||
'commit': True, # otheriwse the deposit does not go through
|
||
}
|
||
response = await self.v2PrivatePostAccountsAccountIdDeposits(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "data": {
|
||
# "id": "67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "status": "created",
|
||
# "payment_method": {
|
||
# "id": "83562370-3e5c-51db-87da-752af5ab9559",
|
||
# "resource": "payment_method",
|
||
# "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559"
|
||
# },
|
||
# "transaction": {
|
||
# "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a"
|
||
# },
|
||
# "amount": {
|
||
# "amount": "10.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "subtotal": {
|
||
# "amount": "10.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "created_at": "2015-01-31T20:49:02Z",
|
||
# "updated_at": "2015-02-11T16:54:02-08:00",
|
||
# "resource": "deposit",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "committed": True,
|
||
# "fee": {
|
||
# "amount": "0.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "payout_at": "2015-02-18T16:54:00-08:00"
|
||
# }
|
||
# }
|
||
#
|
||
# https://github.com/ccxt/ccxt/issues/25484
|
||
data = self.safe_dict_2(response, 'data', 'transfer', {})
|
||
return self.parse_transaction(data)
|
||
|
||
async def fetch_deposit(self, id: str, code: Str = None, params={}):
|
||
"""
|
||
fetch information on a deposit, fiat only, for crypto transactions use fetchLedger
|
||
|
||
https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-deposits#show-deposit
|
||
|
||
:param str id: deposit id
|
||
:param str [code]: unified currency code
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.accountId]: the id of the account that the funds were deposited into
|
||
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
accountId = self.safe_string_2(params, 'account_id', 'accountId')
|
||
params = self.omit(params, ['account_id', 'accountId'])
|
||
if accountId is None:
|
||
if code is None:
|
||
raise ArgumentsRequired(self.id + ' fetchDeposit() requires an account_id(or accountId) parameter OR a currency code argument')
|
||
accountId = await self.find_account_id(code, params)
|
||
if accountId is None:
|
||
raise ExchangeError(self.id + ' fetchDeposit() could not find account id for ' + code)
|
||
request: dict = {
|
||
'account_id': accountId,
|
||
'deposit_id': id,
|
||
}
|
||
response = await self.v2PrivateGetAccountsAccountIdDepositsDepositId(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "data": {
|
||
# "id": "67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "status": "completed",
|
||
# "payment_method": {
|
||
# "id": "83562370-3e5c-51db-87da-752af5ab9559",
|
||
# "resource": "payment_method",
|
||
# "resource_path": "/v2/payment-methods/83562370-3e5c-51db-87da-752af5ab9559"
|
||
# },
|
||
# "transaction": {
|
||
# "id": "441b9494-b3f0-5b98-b9b0-4d82c21c252a",
|
||
# "resource": "transaction",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/transactions/441b9494-b3f0-5b98-b9b0-4d82c21c252a"
|
||
# },
|
||
# "amount": {
|
||
# "amount": "10.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "subtotal": {
|
||
# "amount": "10.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "created_at": "2015-01-31T20:49:02Z",
|
||
# "updated_at": "2015-02-11T16:54:02-08:00",
|
||
# "resource": "deposit",
|
||
# "resource_path": "/v2/accounts/2bbf394c-193b-5b2a-9155-3b4732659ede/deposits/67e0eaec-07d7-54c4-a72c-2e92826897df",
|
||
# "committed": True,
|
||
# "fee": {
|
||
# "amount": "0.00",
|
||
# "currency": "USD"
|
||
# },
|
||
# "payout_at": "2015-02-18T16:54:00-08:00"
|
||
# }
|
||
# }
|
||
#
|
||
# https://github.com/ccxt/ccxt/issues/25484
|
||
data = self.safe_dict_2(response, 'data', 'transfer', {})
|
||
return self.parse_transaction(data)
|
||
|
||
async def fetch_deposit_method_ids(self, params={}):
|
||
"""
|
||
fetch the deposit id for a fiat currency associated with self account
|
||
|
||
https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethods
|
||
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:returns dict: an array of `deposit id structures <https://docs.ccxt.com/#/?id=deposit-id-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
response = await self.v3PrivateGetBrokeragePaymentMethods(params)
|
||
#
|
||
# {
|
||
# "payment_methods": [
|
||
# {
|
||
# "id": "21b39a5d-f7b46876fb2e",
|
||
# "type": "COINBASE_FIAT_ACCOUNT",
|
||
# "name": "CAD Wallet",
|
||
# "currency": "CAD",
|
||
# "verified": True,
|
||
# "allow_buy": False,
|
||
# "allow_sell": True,
|
||
# "allow_deposit": False,
|
||
# "allow_withdraw": False,
|
||
# "created_at": "2023-06-29T19:58:46Z",
|
||
# "updated_at": "2023-10-30T20:25:01Z"
|
||
# }
|
||
# ]
|
||
# }
|
||
#
|
||
result = self.safe_list(response, 'payment_methods', [])
|
||
return self.parse_deposit_method_ids(result)
|
||
|
||
async def fetch_deposit_method_id(self, id: str, params={}):
|
||
"""
|
||
fetch the deposit id for a fiat currency associated with self account
|
||
|
||
https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_getpaymentmethod
|
||
|
||
:param str id: the deposit payment method id
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:returns dict: a `deposit id structure <https://docs.ccxt.com/#/?id=deposit-id-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
request: dict = {
|
||
'payment_method_id': id,
|
||
}
|
||
response = await self.v3PrivateGetBrokeragePaymentMethodsPaymentMethodId(self.extend(request, params))
|
||
#
|
||
# {
|
||
# "payment_method": {
|
||
# "id": "21b39a5d-f7b46876fb2e",
|
||
# "type": "COINBASE_FIAT_ACCOUNT",
|
||
# "name": "CAD Wallet",
|
||
# "currency": "CAD",
|
||
# "verified": True,
|
||
# "allow_buy": False,
|
||
# "allow_sell": True,
|
||
# "allow_deposit": False,
|
||
# "allow_withdraw": False,
|
||
# "created_at": "2023-06-29T19:58:46Z",
|
||
# "updated_at": "2023-10-30T20:25:01Z"
|
||
# }
|
||
# }
|
||
#
|
||
result = self.safe_dict(response, 'payment_method', {})
|
||
return self.parse_deposit_method_id(result)
|
||
|
||
def parse_deposit_method_ids(self, ids, params={}):
|
||
result = []
|
||
for i in range(0, len(ids)):
|
||
id = self.extend(self.parse_deposit_method_id(ids[i]), params)
|
||
result.append(id)
|
||
return result
|
||
|
||
def parse_deposit_method_id(self, depositId):
|
||
return {
|
||
'info': depositId,
|
||
'id': self.safe_string(depositId, 'id'),
|
||
'currency': self.safe_string(depositId, 'currency'),
|
||
'verified': self.safe_bool(depositId, 'verified'),
|
||
'tag': self.safe_string(depositId, 'name'),
|
||
}
|
||
|
||
async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion:
|
||
"""
|
||
fetch a quote for converting from one currency to another
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_createconvertquote
|
||
|
||
:param str fromCode: the currency that you want to sell and convert from
|
||
:param str toCode: the currency that you want to buy and convert into
|
||
:param float [amount]: how much you want to trade in units of the from currency
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param dict [params.trade_incentive_metadata]: an object to fill in user incentive data
|
||
:param str [params.trade_incentive_metadata.user_incentive_id]: the id of the incentive
|
||
:param str [params.trade_incentive_metadata.code_val]: the code value of the incentive
|
||
:returns dict: a `conversion structure <https://docs.ccxt.com/#/?id=conversion-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
request: dict = {
|
||
'from_account': fromCode,
|
||
'to_account': toCode,
|
||
'amount': self.number_to_string(amount),
|
||
}
|
||
response = await self.v3PrivatePostBrokerageConvertQuote(self.extend(request, params))
|
||
data = self.safe_dict(response, 'trade', {})
|
||
return self.parse_conversion(data)
|
||
|
||
async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion:
|
||
"""
|
||
convert from one currency to another
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_commitconverttrade
|
||
|
||
:param str id: the id of the trade that you want to make
|
||
:param str fromCode: the currency that you want to sell and convert from
|
||
:param str toCode: the currency that you want to buy and convert into
|
||
:param float [amount]: how much you want to trade in units of the from currency
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:returns dict: a `conversion structure <https://docs.ccxt.com/#/?id=conversion-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
request: dict = {
|
||
'trade_id': id,
|
||
'from_account': fromCode,
|
||
'to_account': toCode,
|
||
}
|
||
response = await self.v3PrivatePostBrokerageConvertTradeTradeId(self.extend(request, params))
|
||
data = self.safe_dict(response, 'trade', {})
|
||
return self.parse_conversion(data)
|
||
|
||
async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion:
|
||
"""
|
||
fetch the data for a conversion trade
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getconverttrade
|
||
|
||
:param str id: the id of the trade that you want to commit
|
||
:param str code: the unified currency code that was converted from
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param strng params['toCode']: the unified currency code that was converted into
|
||
:returns dict: a `conversion structure <https://docs.ccxt.com/#/?id=conversion-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
if code is None:
|
||
raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a code argument')
|
||
toCode = self.safe_string(params, 'toCode')
|
||
if toCode is None:
|
||
raise ArgumentsRequired(self.id + ' fetchConvertTrade() requires a toCode parameter')
|
||
params = self.omit(params, 'toCode')
|
||
request: dict = {
|
||
'trade_id': id,
|
||
'from_account': code,
|
||
'to_account': toCode,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageConvertTradeTradeId(self.extend(request, params))
|
||
data = self.safe_dict(response, 'trade', {})
|
||
return self.parse_conversion(data)
|
||
|
||
def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion:
|
||
fromCoin = self.safe_string(conversion, 'source_currency')
|
||
fromCode = self.safe_currency_code(fromCoin, fromCurrency)
|
||
to = self.safe_string(conversion, 'target_currency')
|
||
toCode = self.safe_currency_code(to, toCurrency)
|
||
fromAmountStructure = self.safe_dict(conversion, 'user_entered_amount')
|
||
feeStructure = self.safe_dict(conversion, 'total_fee')
|
||
feeAmountStructure = self.safe_dict(feeStructure, 'amount')
|
||
return {
|
||
'info': conversion,
|
||
'timestamp': None,
|
||
'datetime': None,
|
||
'id': self.safe_string(conversion, 'id'),
|
||
'fromCurrency': fromCode,
|
||
'fromAmount': self.safe_number(fromAmountStructure, 'value'),
|
||
'toCurrency': toCode,
|
||
'toAmount': None,
|
||
'price': None,
|
||
'fee': self.safe_number(feeAmountStructure, 'value'),
|
||
}
|
||
|
||
async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order:
|
||
"""
|
||
*futures only* closes open positions for a market
|
||
|
||
https://docs.cdp.coinbase.com/coinbase-app/trade/reference/retailbrokerageapi_closeposition
|
||
|
||
:param str symbol: Unified CCXT market symbol
|
||
:param str [side]: not used by coinbase
|
||
:param dict [params]: extra parameters specific to the coinbase api endpoint
|
||
@param {str} params.clientOrderId *mandatory* the client order id of the position to close
|
||
:param float [params.size]: the size of the position to close, optional
|
||
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId')
|
||
params = self.omit(params, 'clientOrderId')
|
||
request: dict = {
|
||
'product_id': market['id'],
|
||
}
|
||
if clientOrderId is None:
|
||
raise ArgumentsRequired(self.id + ' closePosition() requires a clientOrderId parameter')
|
||
request['client_order_id'] = clientOrderId
|
||
response = await self.v3PrivatePostBrokerageOrdersClosePosition(self.extend(request, params))
|
||
order = self.safe_dict(response, 'success_response', {})
|
||
return self.parse_order(order)
|
||
|
||
async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
||
"""
|
||
fetch all open positions
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmpositions
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxpositions
|
||
|
||
:param str[] [symbols]: list of unified market symbols
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.portfolio]: the portfolio UUID to fetch positions for
|
||
:returns dict[]: a list of `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
symbols = self.market_symbols(symbols)
|
||
market = None
|
||
if symbols is not None:
|
||
market = self.market(symbols[0])
|
||
type = None
|
||
type, params = self.handle_market_type_and_params('fetchPositions', market, params)
|
||
response = None
|
||
if type == 'future':
|
||
response = await self.v3PrivateGetBrokerageCfmPositions(params)
|
||
else:
|
||
portfolio = None
|
||
portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio')
|
||
if portfolio is None:
|
||
raise ArgumentsRequired(self.id + ' fetchPositions() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()')
|
||
request: dict = {
|
||
'portfolio_uuid': portfolio,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageIntxPositionsPortfolioUuid(self.extend(request, params))
|
||
positions = self.safe_list(response, 'positions', [])
|
||
return self.parse_positions(positions, symbols)
|
||
|
||
async def fetch_position(self, symbol: str, params={}):
|
||
"""
|
||
fetch data on a single open contract trade position
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getintxposition
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getfcmposition
|
||
|
||
:param str symbol: unified market symbol of the market the position is held in, default is None
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.product_id]: *futures only* the product id of the position to fetch, required for futures markets only
|
||
:param str [params.portfolio]: *perpetual/swaps only* the portfolio UUID to fetch the position for, required for perpetual/swaps markets only
|
||
:returns dict: a `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
||
"""
|
||
await self.load_markets()
|
||
market = self.market(symbol)
|
||
response = None
|
||
if market['future']:
|
||
productId = self.safe_string(market, 'product_id')
|
||
if productId is None:
|
||
raise ArgumentsRequired(self.id + ' fetchPosition() requires a "product_id" in params')
|
||
futureRequest: dict = {
|
||
'product_id': productId,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageCfmPositionsProductId(self.extend(futureRequest, params))
|
||
else:
|
||
portfolio = None
|
||
portfolio, params = self.handle_option_and_params(params, 'fetchPositions', 'portfolio')
|
||
if portfolio is None:
|
||
raise ArgumentsRequired(self.id + ' fetchPosition() requires a "portfolio" value in params(eg: dbcb91e7-2bc9-515), or set.options["portfolio"]. You can get a list of portfolios with fetchPortfolios()')
|
||
request: dict = {
|
||
'symbol': market['id'],
|
||
'portfolio_uuid': portfolio,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageIntxPositionsPortfolioUuidSymbol(self.extend(request, params))
|
||
position = self.safe_dict(response, 'position', {})
|
||
return self.parse_position(position, market)
|
||
|
||
def parse_position(self, position: dict, market: Market = None):
|
||
#
|
||
# {
|
||
# "product_id": "1r4njf84-0-0",
|
||
# "product_uuid": "cd34c18b-3665-4ed8-9305-3db277c49fc5",
|
||
# "symbol": "ADA-PERP-INTX",
|
||
# "vwap": {
|
||
# "value": "0.6171",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "position_side": "POSITION_SIDE_LONG",
|
||
# "net_size": "20",
|
||
# "buy_order_size": "0",
|
||
# "sell_order_size": "0",
|
||
# "im_contribution": "0.1",
|
||
# "unrealized_pnl": {
|
||
# "value": "0.074",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "mark_price": {
|
||
# "value": "0.6208",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "liquidation_price": {
|
||
# "value": "0",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "leverage": "1",
|
||
# "im_notional": {
|
||
# "value": "12.342",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "mm_notional": {
|
||
# "value": "0.814572",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "position_notional": {
|
||
# "value": "12.342",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "margin_type": "MARGIN_TYPE_CROSS",
|
||
# "liquidation_buffer": "19.677828",
|
||
# "liquidation_percentage": "4689.3506",
|
||
# "portfolio_summary": {
|
||
# "portfolio_uuid": "018ebd63-1f6d-7c8e-ada9-0761c5a2235f",
|
||
# "collateral": "20.4184",
|
||
# "position_notional": "12.342",
|
||
# "open_position_notional": "12.342",
|
||
# "pending_fees": "0",
|
||
# "borrow": "0",
|
||
# "accrued_interest": "0",
|
||
# "rolling_debt": "0",
|
||
# "portfolio_initial_margin": "0.1",
|
||
# "portfolio_im_notional": {
|
||
# "value": "12.342",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "portfolio_maintenance_margin": "0.066",
|
||
# "portfolio_mm_notional": {
|
||
# "value": "0.814572",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "liquidation_percentage": "4689.3506",
|
||
# "liquidation_buffer": "19.677828",
|
||
# "margin_type": "MARGIN_TYPE_CROSS",
|
||
# "margin_flags": "PORTFOLIO_MARGIN_FLAGS_UNSPECIFIED",
|
||
# "liquidation_status": "PORTFOLIO_LIQUIDATION_STATUS_NOT_LIQUIDATING",
|
||
# "unrealized_pnl": {
|
||
# "value": "0.074",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "buying_power": {
|
||
# "value": "8.1504",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "total_balance": {
|
||
# "value": "20.4924",
|
||
# "currency": "USDC"
|
||
# },
|
||
# "max_withdrawal": {
|
||
# "value": "8.0764",
|
||
# "currency": "USDC"
|
||
# }
|
||
# },
|
||
# "entry_vwap": {
|
||
# "value": "0.6091",
|
||
# "currency": "USDC"
|
||
# }
|
||
# }
|
||
#
|
||
marketId = self.safe_string(position, 'symbol', '')
|
||
market = self.safe_market(marketId, market)
|
||
rawMargin = self.safe_string(position, 'margin_type')
|
||
marginMode = None
|
||
if rawMargin is not None:
|
||
marginMode = 'cross' if (rawMargin == 'MARGIN_TYPE_CROSS') else 'isolated'
|
||
notionalObject = self.safe_dict(position, 'position_notional', {})
|
||
positionSide = self.safe_string(position, 'position_side')
|
||
side = 'long' if (positionSide == 'POSITION_SIDE_LONG') else 'short'
|
||
unrealizedPNLObject = self.safe_dict(position, 'unrealized_pnl', {})
|
||
liquidationPriceObject = self.safe_dict(position, 'liquidation_price', {})
|
||
liquidationPrice = self.safe_number(liquidationPriceObject, 'value')
|
||
vwapObject = self.safe_dict(position, 'vwap', {})
|
||
summaryObject = self.safe_dict(position, 'portfolio_summary', {})
|
||
return self.safe_position({
|
||
'info': position,
|
||
'id': self.safe_string(position, 'product_id'),
|
||
'symbol': self.safe_symbol(marketId, market),
|
||
'notional': self.safe_number(notionalObject, 'value'),
|
||
'marginMode': marginMode,
|
||
'liquidationPrice': liquidationPrice,
|
||
'entryPrice': self.safe_number(vwapObject, 'value'),
|
||
'unrealizedPnl': self.safe_number(unrealizedPNLObject, 'value'),
|
||
'realizedPnl': None,
|
||
'percentage': None,
|
||
'contracts': self.safe_number(position, 'net_size'),
|
||
'contractSize': market['contractSize'],
|
||
'markPrice': None,
|
||
'lastPrice': None,
|
||
'side': side,
|
||
'hedged': None,
|
||
'timestamp': None,
|
||
'datetime': None,
|
||
'lastUpdateTimestamp': None,
|
||
'maintenanceMargin': None,
|
||
'maintenanceMarginPercentage': None,
|
||
'collateral': self.safe_number(summaryObject, 'collateral'),
|
||
'initialMargin': None,
|
||
'initialMarginPercentage': None,
|
||
'leverage': self.safe_number(position, 'leverage'),
|
||
'marginRatio': None,
|
||
'stopLossPrice': None,
|
||
'takeProfitPrice': None,
|
||
})
|
||
|
||
async def fetch_trading_fees(self, params={}) -> TradingFees:
|
||
"""
|
||
|
||
https://docs.cdp.coinbase.com/advanced-trade/reference/retailbrokerageapi_gettransactionsummary/
|
||
|
||
fetch the trading fees for multiple markets
|
||
:param dict [params]: extra parameters specific to the exchange API endpoint
|
||
:param str [params.type]: 'spot' or 'swap'
|
||
:returns dict: a dictionary of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>` indexed by market symbols
|
||
"""
|
||
await self.load_markets()
|
||
type = None
|
||
type, params = self.handle_market_type_and_params('fetchTradingFees', None, params)
|
||
isSpot = (type == 'spot')
|
||
productType = 'SPOT' if isSpot else 'FUTURE'
|
||
request: dict = {
|
||
'product_type': productType,
|
||
}
|
||
response = await self.v3PrivateGetBrokerageTransactionSummary(self.extend(request, params))
|
||
#
|
||
# {
|
||
# total_volume: '0',
|
||
# total_fees: '0',
|
||
# fee_tier: {
|
||
# pricing_tier: 'Advanced 1',
|
||
# usd_from: '0',
|
||
# usd_to: '1000',
|
||
# taker_fee_rate: '0.008',
|
||
# maker_fee_rate: '0.006',
|
||
# aop_from: '',
|
||
# aop_to: ''
|
||
# },
|
||
# margin_rate: null,
|
||
# goods_and_services_tax: null,
|
||
# advanced_trade_only_volume: '0',
|
||
# advanced_trade_only_fees: '0',
|
||
# coinbase_pro_volume: '0',
|
||
# coinbase_pro_fees: '0',
|
||
# total_balance: '',
|
||
# has_promo_fee: False
|
||
# }
|
||
#
|
||
data = self.safe_dict(response, 'fee_tier', {})
|
||
taker_fee = self.safe_number(data, 'taker_fee_rate')
|
||
marker_fee = self.safe_number(data, 'maker_fee_rate')
|
||
result: dict = {}
|
||
for i in range(0, len(self.symbols)):
|
||
symbol = self.symbols[i]
|
||
market = self.market(symbol)
|
||
if (isSpot and market['spot']) or (not isSpot and not market['spot']):
|
||
result[symbol] = {
|
||
'info': response,
|
||
'symbol': symbol,
|
||
'maker': taker_fee,
|
||
'taker': marker_fee,
|
||
'percentage': True,
|
||
}
|
||
return result
|
||
|
||
async def fetch_portfolio_details(self, portfolioUuid: str, params={}) -> List[Any]:
|
||
"""
|
||
Fetch details for a specific portfolio by UUID
|
||
|
||
https://docs.cloud.coinbase.com/advanced-trade/reference/retailbrokerageapi_getportfolios
|
||
|
||
:param str portfolioUuid: The unique identifier of the portfolio to fetch
|
||
:param Dict [params]: Extra parameters specific to the exchange API endpoint
|
||
:returns any[]: An account structure <https://docs.ccxt.com/#/?id=account-structure>
|
||
"""
|
||
await self.load_markets()
|
||
request = {
|
||
'portfolio_uuid': portfolioUuid,
|
||
}
|
||
response = await self.v3PrivateGetBrokeragePortfoliosPortfolioUuid(self.extend(request, params))
|
||
result = self.parse_portfolio_details(response)
|
||
return result
|
||
|
||
def parse_portfolio_details(self, portfolioData: dict):
|
||
breakdown = portfolioData['breakdown']
|
||
portfolioInfo = self.safe_dict(breakdown, 'portfolio', {})
|
||
portfolioName = self.safe_string(portfolioInfo, 'name', 'Unknown')
|
||
portfolioUuid = self.safe_string(portfolioInfo, 'uuid', '')
|
||
spotPositions = self.safe_list(breakdown, 'spot_positions', [])
|
||
parsedPositions = []
|
||
for i in range(0, len(spotPositions)):
|
||
position: dict = spotPositions[i]
|
||
currencyCode = self.safe_string(position, 'asset', 'Unknown')
|
||
availableBalanceStr = self.safe_string(position, 'available_to_trade_fiat', '0')
|
||
availableBalance = self.parse_number(availableBalanceStr)
|
||
totalBalanceFiatStr = self.safe_string(position, 'total_balance_fiat', '0')
|
||
totalBalanceFiat = self.parse_number(totalBalanceFiatStr)
|
||
holdAmount = totalBalanceFiat - availableBalance
|
||
costBasisDict = self.safe_dict(position, 'cost_basis', {})
|
||
costBasisStr = self.safe_string(costBasisDict, 'value', '0')
|
||
averageEntryPriceDict = self.safe_dict(position, 'average_entry_price', {})
|
||
averageEntryPriceStr = self.safe_string(averageEntryPriceDict, 'value', '0')
|
||
positionData: dict = {
|
||
'currency': currencyCode,
|
||
'available_balance': availableBalance,
|
||
'hold_amount': holdAmount > holdAmount if 0 else 0,
|
||
'wallet_name': portfolioName,
|
||
'account_id': portfolioUuid,
|
||
'account_uuid': self.safe_string(position, 'account_uuid', ''),
|
||
'total_balance_fiat': totalBalanceFiat,
|
||
'total_balance_crypto': self.parse_number(self.safe_string(position, 'total_balance_crypto', '0')),
|
||
'available_to_trade_fiat': self.parse_number(self.safe_string(position, 'available_to_trade_fiat', '0')),
|
||
'available_to_trade_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')),
|
||
'available_to_transfer_fiat': self.parse_number(self.safe_string(position, 'available_to_transfer_fiat', '0')),
|
||
'available_to_transfer_crypto': self.parse_number(self.safe_string(position, 'available_to_trade_crypto', '0')),
|
||
'allocation': self.parse_number(self.safe_string(position, 'allocation', '0')),
|
||
'cost_basis': self.parse_number(costBasisStr),
|
||
'cost_basis_currency': self.safe_string(costBasisDict, 'currency', 'USD'),
|
||
'is_cash': self.safe_bool(position, 'is_cash', False),
|
||
'average_entry_price': self.parse_number(averageEntryPriceStr),
|
||
'average_entry_price_currency': self.safe_string(averageEntryPriceDict, 'currency', 'USD'),
|
||
'asset_uuid': self.safe_string(position, 'asset_uuid', ''),
|
||
'unrealized_pnl': self.parse_number(self.safe_string(position, 'unrealized_pnl', '0')),
|
||
'asset_color': self.safe_string(position, 'asset_color', ''),
|
||
'account_type': self.safe_string(position, 'account_type', ''),
|
||
}
|
||
parsedPositions.append(positionData)
|
||
return parsedPositions
|
||
|
||
def create_auth_token(self, seconds: Int, method: Str = None, url: Str = None, useEddsa=False):
|
||
# v1 https://docs.cdp.coinbase.com/api-reference/authentication#php-2
|
||
# v2 https://docs.cdp.coinbase.com/api-reference/v2/authentication
|
||
uri = None
|
||
if url is not None:
|
||
uri = method + ' ' + url.replace('https://', '')
|
||
quesPos = uri.find('?')
|
||
# Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True
|
||
# Also it's not possible that the question mark is first character, only check > 0 here.
|
||
if quesPos > 0:
|
||
uri = uri[0:quesPos]
|
||
# self.eddsa{"sub":"d2efa49a-369c-43d7-a60e-ae26e28853c2","iss":"cdp","aud":["cdp_service"],"uris":["GET api.coinbase.com/api/v3/brokerage/transaction_summary"]}
|
||
nonce = self.random_bytes(16)
|
||
aud = 'cdp_service' if useEddsa else 'retail_rest_api_proxy'
|
||
iss = 'cdp' if useEddsa else 'coinbase-cloud'
|
||
request: dict = {
|
||
'aud': [aud],
|
||
'iss': iss,
|
||
'nbf': seconds,
|
||
'exp': seconds + 120,
|
||
'sub': self.apiKey,
|
||
'iat': seconds,
|
||
}
|
||
if uri is not None:
|
||
if not useEddsa:
|
||
request['uri'] = uri
|
||
else:
|
||
request['uris'] = [uri]
|
||
if useEddsa:
|
||
byteArray = self.base64_to_binary(self.secret)
|
||
seed = self.array_slice(byteArray, 0, 32)
|
||
return self.jwt(request, seed, 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'EdDSA'})
|
||
else:
|
||
# self.ecdsawith p256
|
||
return self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'})
|
||
|
||
def nonce(self):
|
||
return self.milliseconds() - self.options['timeDifference']
|
||
|
||
def sign(self, path, api=[], method='GET', params={}, headers=None, body=None):
|
||
version = api[0]
|
||
signed = api[1] == 'private'
|
||
isV3 = version == 'v3'
|
||
pathPart = 'api/v3' if (isV3) else 'v2'
|
||
fullPath = '/' + pathPart + '/' + self.implode_params(path, params)
|
||
query = self.omit(params, self.extract_params(path))
|
||
savedPath = fullPath
|
||
if method == 'GET':
|
||
if query:
|
||
fullPath += '?' + self.urlencode_with_array_repeat(query)
|
||
url = self.urls['api']['rest'] + fullPath
|
||
if signed:
|
||
authorization = self.safe_string(self.headers, 'Authorization')
|
||
authorizationString = None
|
||
if authorization is not None:
|
||
authorizationString = authorization
|
||
elif self.token and not self.check_required_credentials(False):
|
||
authorizationString = 'Bearer ' + self.token
|
||
else:
|
||
self.check_required_credentials()
|
||
seconds = self.seconds()
|
||
payload = ''
|
||
if method != 'GET':
|
||
if query:
|
||
body = self.json(query)
|
||
payload = body
|
||
else:
|
||
if not isV3:
|
||
if query:
|
||
payload += '?' + self.urlencode(query)
|
||
# v3: 'GET' doesn't need payload in the signature. inside url is enough
|
||
# https://docs.cloud.coinbase.com/advanced-trade/docs/auth#example-request
|
||
# v2: 'GET' require payload in the signature
|
||
# https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-key-authentication
|
||
isCloudAPiKey = (self.apiKey.find('organizations/') >= 0) or (self.secret.startswith('-----BEGIN'))
|
||
# using the size might be fragile, so we add an option to force v2 cloud api key if needed
|
||
isV2CloudAPiKey = len(self.secret) == 88 or self.safe_bool(self.options, 'v2CloudAPiKey', False) or self.secret.endswith('=')
|
||
if isCloudAPiKey or isV2CloudAPiKey:
|
||
if isCloudAPiKey and self.apiKey.startswith('-----BEGIN'):
|
||
raise ArgumentsRequired(self.id + ' apiKey should contain the name(eg: organizations/3b910e93....) and not the public key')
|
||
# # it may not work for v2
|
||
# uri = method + ' ' + url.replace('https://', '')
|
||
# quesPos = uri.find('?')
|
||
# # Due to we use mb_strpos, quesPos could be False in php. In that case, the quesPos >= 0 is True
|
||
# # Also it's not possible that the question mark is first character, only check > 0 here.
|
||
# if quesPos > 0:
|
||
# uri = uri[0:quesPos]
|
||
# }
|
||
# nonce = self.random_bytes(16)
|
||
# request: Dict = {
|
||
# 'aud': ['retail_rest_api_proxy'],
|
||
# 'iss': 'coinbase-cloud',
|
||
# 'nbf': seconds,
|
||
# 'exp': seconds + 120,
|
||
# 'sub': self.apiKey,
|
||
# 'uri': uri,
|
||
# 'iat': seconds,
|
||
# }
|
||
token = self.create_auth_token(seconds, method, url, isV2CloudAPiKey)
|
||
# token = self.jwt(request, self.encode(self.secret), 'sha256', False, {'kid': self.apiKey, 'nonce': nonce, 'alg': 'ES256'})
|
||
authorizationString = 'Bearer ' + token
|
||
else:
|
||
nonce = self.nonce()
|
||
timestamp = self.parse_to_int(nonce / 1000)
|
||
timestampString = str(timestamp)
|
||
auth = timestampString + method + savedPath + payload
|
||
signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)
|
||
headers = {
|
||
'CB-ACCESS-KEY': self.apiKey,
|
||
'CB-ACCESS-SIGN': signature,
|
||
'CB-ACCESS-TIMESTAMP': timestampString,
|
||
'Content-Type': 'application/json',
|
||
}
|
||
if authorizationString is not None:
|
||
headers = {
|
||
'Authorization': authorizationString,
|
||
'Content-Type': 'application/json',
|
||
}
|
||
if method != 'GET':
|
||
if query:
|
||
body = self.json(query)
|
||
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 response is None:
|
||
return None # fallback to default error handler
|
||
feedback = self.id + ' ' + body
|
||
#
|
||
# {"error": "invalid_request", "error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."}
|
||
#
|
||
# or
|
||
#
|
||
# {
|
||
# "errors": [
|
||
# {
|
||
# "id": "not_found",
|
||
# "message": "Not found"
|
||
# }
|
||
# ]
|
||
# }
|
||
# or
|
||
# {
|
||
# "success": False,
|
||
# "error_response": {
|
||
# "error": "UNKNOWN_FAILURE_REASON",
|
||
# "message": "",
|
||
# "error_details": "",
|
||
# "preview_failure_reason": "PREVIEW_STOP_PRICE_ABOVE_LAST_TRADE_PRICE"
|
||
# },
|
||
# "order_configuration": {
|
||
# "stop_limit_stop_limit_gtc": {
|
||
# "base_size": "0.0001",
|
||
# "limit_price": "2000",
|
||
# "stop_price": "2005",
|
||
# "stop_direction": "STOP_DIRECTION_STOP_DOWN",
|
||
# "reduce_only": False
|
||
# }
|
||
# }
|
||
# }
|
||
#
|
||
errorCode = self.safe_string(response, 'error')
|
||
if errorCode is not None:
|
||
errorMessage = self.safe_string_2(response, 'error_description', 'error')
|
||
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
||
self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback)
|
||
raise ExchangeError(feedback)
|
||
errorResponse = self.safe_dict(response, 'error_response')
|
||
if errorResponse is not None:
|
||
errorMessageInner = self.safe_string_2(errorResponse, 'preview_failure_reason', 'preview_failure_reason')
|
||
self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessageInner, feedback)
|
||
self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessageInner, feedback)
|
||
raise ExchangeError(feedback)
|
||
errors = self.safe_list(response, 'errors')
|
||
if errors is not None:
|
||
if isinstance(errors, list):
|
||
numErrors = len(errors)
|
||
if numErrors > 0:
|
||
errorCode = self.safe_string(errors[0], 'id')
|
||
errorMessage = self.safe_string(errors[0], 'message')
|
||
if errorCode is not None:
|
||
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
||
self.throw_broadly_matched_exception(self.exceptions['broad'], errorMessage, feedback)
|
||
raise ExchangeError(feedback)
|
||
advancedTrade = self.options['advanced']
|
||
if not ('data' in response) and (not advancedTrade):
|
||
raise ExchangeError(self.id + ' failed due to a malformed response ' + self.json(response))
|
||
return None
|