3444 lines
149 KiB
Python
3444 lines
149 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
from ccxt.base.exchange import Exchange
|
|
from ccxt.abstract.kraken import ImplicitAPI
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import PermissionDenied
|
|
from ccxt.base.errors import AccountSuspended
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidAddress
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.errors import DDoSProtection
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.errors import ExchangeNotAvailable
|
|
from ccxt.base.errors import OnMaintenance
|
|
from ccxt.base.errors import InvalidNonce
|
|
from ccxt.base.errors import CancelPending
|
|
from ccxt.base.decimal_to_precision import TRUNCATE
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class kraken(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(kraken, self).describe(), {
|
|
'id': 'kraken',
|
|
'name': 'Kraken',
|
|
'countries': ['US'],
|
|
'version': '0',
|
|
# rate-limits: https://support.kraken.com/hc/en-us/articles/206548367-What-are-the-API-rate-limits-#1
|
|
# for public: 1 req/s
|
|
# for private: every second 0.33 weight added to your allowed capacity(some private endpoints need 1 weight, some need 2)
|
|
'rateLimit': 1000,
|
|
'certified': False,
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': True,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'cancelAllOrders': True,
|
|
'cancelAllOrdersAfter': True,
|
|
'cancelOrder': True,
|
|
'cancelOrders': True,
|
|
'createDepositAddress': True,
|
|
'createMarketBuyOrderWithCost': True,
|
|
'createMarketOrderWithCost': False,
|
|
'createMarketSellOrderWithCost': False,
|
|
'createOrder': True,
|
|
'createOrders': True,
|
|
'createStopLimitOrder': True,
|
|
'createStopMarketOrder': True,
|
|
'createStopOrder': True,
|
|
'createTrailingAmountOrder': True,
|
|
'createTrailingPercentOrder': True,
|
|
'editOrder': True,
|
|
'fetchBalance': True,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDepositAddressesByNetwork': False,
|
|
'fetchDeposits': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchLedger': True,
|
|
'fetchLedgerEntry': True,
|
|
'fetchLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrderTrades': 'emulated',
|
|
'fetchPositions': True,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchStatus': True,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTime': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': True,
|
|
'fetchTradingFees': False,
|
|
'fetchWithdrawals': True,
|
|
'setLeverage': False,
|
|
'setMarginMode': False, # Kraken only supports cross margin
|
|
'transfer': True,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': 1,
|
|
'5m': 5,
|
|
'15m': 15,
|
|
'30m': 30,
|
|
'1h': 60,
|
|
'4h': 240,
|
|
'1d': 1440,
|
|
'1w': 10080,
|
|
'2w': 21600,
|
|
},
|
|
'urls': {
|
|
'logo': 'https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg',
|
|
'api': {
|
|
'public': 'https://api.kraken.com',
|
|
'private': 'https://api.kraken.com',
|
|
'zendesk': 'https://kraken.zendesk.com/api/v2/help_center/en-us/articles', # use the public zendesk api to receive article bodies and bypass new anti-spam protections
|
|
},
|
|
'www': 'https://www.kraken.com',
|
|
'doc': 'https://docs.kraken.com/rest/',
|
|
'fees': 'https://www.kraken.com/en-us/features/fee-schedule',
|
|
},
|
|
'fees': {
|
|
'trading': {
|
|
'tierBased': True,
|
|
'percentage': True,
|
|
'taker': self.parse_number('0.0026'),
|
|
'maker': self.parse_number('0.0016'),
|
|
'tiers': {
|
|
'taker': [
|
|
[self.parse_number('0'), self.parse_number('0.0026')],
|
|
[self.parse_number('50000'), self.parse_number('0.0024')],
|
|
[self.parse_number('100000'), self.parse_number('0.0022')],
|
|
[self.parse_number('250000'), self.parse_number('0.0020')],
|
|
[self.parse_number('500000'), self.parse_number('0.0018')],
|
|
[self.parse_number('1000000'), self.parse_number('0.0016')],
|
|
[self.parse_number('2500000'), self.parse_number('0.0014')],
|
|
[self.parse_number('5000000'), self.parse_number('0.0012')],
|
|
[self.parse_number('10000000'), self.parse_number('0.0001')],
|
|
],
|
|
'maker': [
|
|
[self.parse_number('0'), self.parse_number('0.0016')],
|
|
[self.parse_number('50000'), self.parse_number('0.0014')],
|
|
[self.parse_number('100000'), self.parse_number('0.0012')],
|
|
[self.parse_number('250000'), self.parse_number('0.0010')],
|
|
[self.parse_number('500000'), self.parse_number('0.0008')],
|
|
[self.parse_number('1000000'), self.parse_number('0.0006')],
|
|
[self.parse_number('2500000'), self.parse_number('0.0004')],
|
|
[self.parse_number('5000000'), self.parse_number('0.0002')],
|
|
[self.parse_number('10000000'), self.parse_number('0.0')],
|
|
],
|
|
},
|
|
},
|
|
},
|
|
'handleContentTypeApplicationZip': True,
|
|
'api': {
|
|
'zendesk': {
|
|
'get': [
|
|
# we should really refrain from putting fixed fee numbers and stop hardcoding
|
|
# we will be using their web APIs to scrape all numbers from these articles
|
|
'360000292886', # -What-are-the-deposit-fees-
|
|
'201893608', # -What-are-the-withdrawal-fees-
|
|
],
|
|
},
|
|
'public': {
|
|
'get': {
|
|
# rate-limits explained in comment in the top of self file
|
|
'Assets': 1,
|
|
'AssetPairs': 1,
|
|
'Depth': 1.2,
|
|
'OHLC': 1.2, # 1.2 because 1 triggers too many requests immediately
|
|
'Spread': 1,
|
|
'SystemStatus': 1,
|
|
'Ticker': 1,
|
|
'Time': 1,
|
|
'Trades': 1.2,
|
|
},
|
|
},
|
|
'private': {
|
|
'post': {
|
|
'AddOrder': 0,
|
|
'AddOrderBatch': 0,
|
|
'AddExport': 3,
|
|
'AmendOrder': 0,
|
|
'Balance': 3,
|
|
'CancelAll': 3,
|
|
'CancelAllOrdersAfter': 3,
|
|
'CancelOrder': 0,
|
|
'CancelOrderBatch': 0,
|
|
'ClosedOrders': 3,
|
|
'DepositAddresses': 3,
|
|
'DepositMethods': 3,
|
|
'DepositStatus': 3,
|
|
'EditOrder': 0,
|
|
'ExportStatus': 3,
|
|
'GetWebSocketsToken': 3,
|
|
'Ledgers': 6,
|
|
'OpenOrders': 3,
|
|
'OpenPositions': 3,
|
|
'QueryLedgers': 3,
|
|
'QueryOrders': 3,
|
|
'QueryTrades': 3,
|
|
'RetrieveExport': 3,
|
|
'RemoveExport': 3,
|
|
'BalanceEx': 3,
|
|
'TradeBalance': 3,
|
|
'TradesHistory': 6,
|
|
'TradeVolume': 3,
|
|
'Withdraw': 3,
|
|
'WithdrawCancel': 3,
|
|
'WithdrawInfo': 3,
|
|
'WithdrawMethods': 3,
|
|
'WithdrawAddresses': 3,
|
|
'WithdrawStatus': 3,
|
|
'WalletTransfer': 3,
|
|
# sub accounts
|
|
'CreateSubaccount': 3,
|
|
'AccountTransfer': 3,
|
|
# earn
|
|
'Earn/Allocate': 3,
|
|
'Earn/Deallocate': 3,
|
|
'Earn/AllocateStatus': 3,
|
|
'Earn/DeallocateStatus': 3,
|
|
'Earn/Strategies': 3,
|
|
'Earn/Allocations': 3,
|
|
},
|
|
},
|
|
},
|
|
'commonCurrencies': {
|
|
# about X & Z prefixes and .S & .M suffixes, see comment under fetchCurrencies
|
|
'LUNA': 'LUNC',
|
|
'LUNA2': 'LUNA',
|
|
'REPV2': 'REP',
|
|
'REP': 'REPV1',
|
|
'UST': 'USTC',
|
|
'XBT': 'BTC',
|
|
'XDG': 'DOGE',
|
|
'FEE': 'KFEE',
|
|
'XETC': 'ETC',
|
|
'XETH': 'ETH',
|
|
'XLTC': 'LTC',
|
|
'XMLN': 'MLN',
|
|
'XREP': 'REP',
|
|
'XXBT': 'BTC',
|
|
'XXDG': 'DOGE',
|
|
'XXLM': 'XLM',
|
|
'XXMR': 'XMR',
|
|
'XXRP': 'XRP',
|
|
'XZEC': 'ZEC',
|
|
'ZAUD': 'AUD',
|
|
'ZCAD': 'CAD',
|
|
'ZEUR': 'EUR',
|
|
'ZGBP': 'GBP',
|
|
'ZJPY': 'JPY',
|
|
'ZUSD': 'USD',
|
|
},
|
|
'options': {
|
|
'timeDifference': 0, # the difference between system clock and Binance clock
|
|
'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation
|
|
'marketsByAltname': {},
|
|
'delistedMarketsById': {},
|
|
# cannot withdraw/deposit these
|
|
'inactiveCurrencies': ['CAD', 'USD', 'JPY', 'GBP'],
|
|
'networks': {
|
|
'ETH': 'ERC20',
|
|
'TRX': 'TRC20',
|
|
},
|
|
'depositMethods': {
|
|
'1INCH': '1inch' + ' ' + '(1INCH)',
|
|
'AAVE': 'Aave',
|
|
'ADA': 'ADA',
|
|
'ALGO': 'Algorand',
|
|
'ANKR': 'ANKR' + ' ' + '(ANKR)',
|
|
'ANT': 'Aragon' + ' ' + '(ANT)',
|
|
'ATOM': 'Cosmos',
|
|
'AXS': 'Axie Infinity Shards' + ' ' + '(AXS)',
|
|
'BADGER': 'Bager DAO' + ' ' + '(BADGER)',
|
|
'BAL': 'Balancer' + ' ' + '(BAL)',
|
|
'BAND': 'Band Protocol' + ' ' + '(BAND)',
|
|
'BAT': 'BAT',
|
|
'BCH': 'Bitcoin Cash',
|
|
'BNC': 'Bifrost' + ' ' + '(BNC)',
|
|
'BNT': 'Bancor' + ' ' + '(BNT)',
|
|
'BTC': 'Bitcoin',
|
|
'CHZ': 'Chiliz' + ' ' + '(CHZ)',
|
|
'COMP': 'Compound' + ' ' + '(COMP)',
|
|
'CQT': '\tCovalent Query Token' + ' ' + '(CQT)',
|
|
'CRV': 'Curve DAO Token' + ' ' + '(CRV)',
|
|
'CTSI': 'Cartesi' + ' ' + '(CTSI)',
|
|
'DAI': 'Dai',
|
|
'DASH': 'Dash',
|
|
'DOGE': 'Dogecoin',
|
|
'DOT': 'Polkadot',
|
|
'DYDX': 'dYdX' + ' ' + '(DYDX)',
|
|
'ENJ': 'Enjin Coin' + ' ' + '(ENJ)',
|
|
'EOS': 'EOS',
|
|
'ETC': 'Ether Classic' + ' ' + '(Hex)',
|
|
'ETH': 'Ether' + ' ' + '(Hex)',
|
|
'EWT': 'Energy Web Token',
|
|
'FEE': 'Kraken Fee Credit',
|
|
'FIL': 'Filecoin',
|
|
'FLOW': 'Flow',
|
|
'GHST': 'Aavegotchi' + ' ' + '(GHST)',
|
|
'GNO': 'GNO',
|
|
'GRT': 'GRT',
|
|
'ICX': 'Icon',
|
|
'INJ': 'Injective Protocol' + ' ' + '(INJ)',
|
|
'KAR': 'Karura' + ' ' + '(KAR)',
|
|
'KAVA': 'Kava',
|
|
'KEEP': 'Keep Token' + ' ' + '(KEEP)',
|
|
'KNC': 'Kyber Network' + ' ' + '(KNC)',
|
|
'KSM': 'Kusama',
|
|
'LINK': 'Link',
|
|
'LPT': 'Livepeer Token' + ' ' + '(LPT)',
|
|
'LRC': 'Loopring' + ' ' + '(LRC)',
|
|
'LSK': 'Lisk',
|
|
'LTC': 'Litecoin',
|
|
'MANA': 'MANA',
|
|
'MATIC': 'Polygon' + ' ' + '(MATIC)',
|
|
'MINA': 'Mina', # inspected from webui
|
|
'MIR': 'Mirror Protocol' + ' ' + '(MIR)',
|
|
'MKR': 'Maker' + ' ' + '(MKR)',
|
|
'MLN': 'MLN',
|
|
'MOVR': 'Moonriver' + ' ' + '(MOVR)',
|
|
'NANO': 'NANO',
|
|
'OCEAN': 'OCEAN',
|
|
'OGN': 'Origin Protocol' + ' ' + '(OGN)',
|
|
'OMG': 'OMG',
|
|
'OXT': 'Orchid' + ' ' + '(OXT)',
|
|
'OXY': 'Oxygen' + ' ' + '(OXY)',
|
|
'PAXG': 'PAX' + ' ' + '(Gold)',
|
|
'PERP': 'Perpetual Protocol' + ' ' + '(PERP)',
|
|
'PHA': 'Phala' + ' ' + '(PHA)',
|
|
'QTUM': 'QTUM',
|
|
'RARI': 'Rarible' + ' ' + '(RARI)',
|
|
'RAY': 'Raydium' + ' ' + '(RAY)',
|
|
'REN': 'Ren Protocol' + ' ' + '(REN)',
|
|
'REP': 'REPv2',
|
|
'REPV1': 'REP',
|
|
'SAND': 'The Sandbox' + ' ' + '(SAND)',
|
|
'SC': 'Siacoin',
|
|
'SDN': 'Shiden' + ' ' + '(SDN)',
|
|
'SOL': 'Solana', # their deposit method api doesn't work for SOL - was guessed
|
|
'SNX': 'Synthetix Network' + ' ' + '(SNX)',
|
|
'SRM': 'Serum', # inspected from webui
|
|
'STORJ': 'Storj' + ' ' + '(STORJ)',
|
|
'SUSHI': 'Sushiswap' + ' ' + '(SUSHI)',
|
|
'TBTC': 'tBTC',
|
|
'TRX': 'Tron',
|
|
'UNI': 'UNI',
|
|
'USDC': 'USDC',
|
|
'USDT': 'Tether USD' + ' ' + '(ERC20)',
|
|
'USDT-TRC20': 'Tether USD' + ' ' + '(TRC20)',
|
|
'WAVES': 'Waves',
|
|
'WBTC': 'Wrapped Bitcoin' + ' ' + '(WBTC)',
|
|
'XLM': 'Stellar XLM',
|
|
'XMR': 'Monero',
|
|
'XRP': 'Ripple XRP',
|
|
'XTZ': 'XTZ',
|
|
'YFI': 'YFI',
|
|
'ZEC': 'Zcash' + ' ' + '(Transparent)',
|
|
'ZRX': '0x' + ' ' + '(ZRX)',
|
|
},
|
|
'withdrawMethods': { # keeping it here because deposit and withdraw return different networks codes
|
|
'Lightning': 'Lightning',
|
|
'Bitcoin': 'BTC',
|
|
'Ripple': 'XRP',
|
|
'Litecoin': 'LTC',
|
|
'Dogecoin': 'DOGE',
|
|
'Stellar': 'XLM',
|
|
'Ethereum': 'ERC20',
|
|
'Arbitrum One': 'Arbitrum',
|
|
'Polygon': 'MATIC',
|
|
'Arbitrum Nova': 'Arbitrum',
|
|
'Optimism': 'Optimism',
|
|
'zkSync Era': 'zkSync',
|
|
'Ethereum Classic': 'ETC',
|
|
'Zcash': 'ZEC',
|
|
'Monero': 'XMR',
|
|
'Tron': 'TRC20',
|
|
'Solana': 'SOL',
|
|
'EOS': 'EOS',
|
|
'Bitcoin Cash': 'BCH',
|
|
'Cardano': 'ADA',
|
|
'Qtum': 'QTUM',
|
|
'Tezos': 'XTZ',
|
|
'Cosmos': 'ATOM',
|
|
'Nano': 'NANO',
|
|
'Siacoin': 'SC',
|
|
'Lisk': 'LSK',
|
|
'Waves': 'WAVES',
|
|
'ICON': 'ICX',
|
|
'Algorand': 'ALGO',
|
|
'Polygon - USDC.e': 'MATIC',
|
|
'Arbitrum One - USDC.e': 'Arbitrum',
|
|
'Polkadot': 'DOT',
|
|
'Kava': 'KAVA',
|
|
'Filecoin': 'FIL',
|
|
'Kusama': 'KSM',
|
|
'Flow': 'FLOW',
|
|
'Energy Web': 'EW',
|
|
'Mina': 'MINA',
|
|
'Centrifuge': 'CFG',
|
|
'Karura': 'KAR',
|
|
'Moonriver': 'MOVR',
|
|
'Shiden': 'SDN',
|
|
'Khala': 'PHA',
|
|
'Bifrost Kusama': 'BNC',
|
|
'Songbird': 'SGB',
|
|
'Terra classic': 'LUNC',
|
|
'KILT': 'KILT',
|
|
'Basilisk': 'BSX',
|
|
'Flare': 'FLR',
|
|
'Avalanche C-Chain': 'AVAX',
|
|
'Kintsugi': 'KINT',
|
|
'Altair': 'AIR',
|
|
'Moonbeam': 'GLMR',
|
|
'Acala': 'ACA',
|
|
'Astar': 'ASTR',
|
|
'Akash': 'AKT',
|
|
'Robonomics': 'XRT',
|
|
'Fantom': 'FTM',
|
|
'Elrond': 'EGLD',
|
|
'THORchain': 'RUNE',
|
|
'Secret': 'SCRT',
|
|
'Near': 'NEAR',
|
|
'Internet Computer Protocol': 'ICP',
|
|
'Picasso': 'PICA',
|
|
'Crust Shadow': 'CSM',
|
|
'Integritee': 'TEER',
|
|
'Parallel Finance': 'PARA',
|
|
'HydraDX': 'HDX',
|
|
'Interlay': 'INTR',
|
|
'Fetch.ai': 'FET',
|
|
'NYM': 'NYM',
|
|
'Terra 2.0': 'LUNA2',
|
|
'Juno': 'JUNO',
|
|
'Nodle': 'NODL',
|
|
'Stacks': 'STX',
|
|
'Ethereum PoW': 'ETHW',
|
|
'Aptos': 'APT',
|
|
'Sui': 'SUI',
|
|
'Genshiro': 'GENS',
|
|
'Aventus': 'AVT',
|
|
'Sei': 'SEI',
|
|
'OriginTrail': 'OTP',
|
|
'Celestia': 'TIA',
|
|
},
|
|
'marketHelperProps': ['marketsByAltname', 'delistedMarketsById'], # used by setMarketsFromExchange
|
|
},
|
|
'features': {
|
|
'spot': {
|
|
'sandbox': False,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': False, # todo
|
|
'triggerPriceType': None,
|
|
'triggerDirection': False,
|
|
'stopLossPrice': True,
|
|
'takeProfitPrice': True,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'hedged': False,
|
|
'trailing': True,
|
|
'leverage': False,
|
|
'marketBuyByCost': True,
|
|
'marketBuyRequiresPrice': False,
|
|
'selfTradePrevention': True, # todo implement
|
|
'iceberg': True, # todo implement
|
|
},
|
|
'createOrders': {
|
|
'min': 2,
|
|
'max': 15,
|
|
'sameSymbolOnly': True,
|
|
},
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': None,
|
|
'daysBack': None,
|
|
'untilDays': None,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': None,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': None,
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': None,
|
|
'daysBack': None,
|
|
'daysBackCanceled': None,
|
|
'untilDays': 100000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 720,
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'exceptions': {
|
|
'exact': {
|
|
'EQuery:Invalid asset pair': BadSymbol, # {"error":["EQuery:Invalid asset pair"]}
|
|
'EAPI:Invalid key': AuthenticationError,
|
|
'EFunding:Unknown withdraw key': InvalidAddress, # {"error":["EFunding:Unknown withdraw key"]}
|
|
'EFunding:Invalid amount': InsufficientFunds,
|
|
'EService:Unavailable': ExchangeNotAvailable,
|
|
'EDatabase:Internal error': ExchangeNotAvailable,
|
|
'EService:Busy': ExchangeNotAvailable,
|
|
'EQuery:Unknown asset': BadSymbol, # {"error":["EQuery:Unknown asset"]}
|
|
'EAPI:Rate limit exceeded': DDoSProtection,
|
|
'EOrder:Rate limit exceeded': DDoSProtection,
|
|
'EGeneral:Internal error': ExchangeNotAvailable,
|
|
'EGeneral:Temporary lockout': DDoSProtection,
|
|
'EGeneral:Permission denied': PermissionDenied,
|
|
'EGeneral:Invalid arguments:price': InvalidOrder,
|
|
'EOrder:Unknown order': InvalidOrder,
|
|
'EOrder:Invalid price:Invalid price argument': InvalidOrder,
|
|
'EOrder:Order minimum not met': InvalidOrder,
|
|
'EOrder:Insufficient funds': InsufficientFunds,
|
|
'EGeneral:Invalid arguments': BadRequest,
|
|
'ESession:Invalid session': AuthenticationError,
|
|
'EAPI:Invalid nonce': InvalidNonce,
|
|
'EFunding:No funding method': BadRequest, # {"error":"EFunding:No funding method"}
|
|
'EFunding:Unknown asset': BadSymbol, # {"error":["EFunding:Unknown asset"]}
|
|
'EService:Market in post_only mode': OnMaintenance, # {"error":["EService:Market in post_only mode"]}
|
|
'EGeneral:Too many requests': DDoSProtection, # {"error":["EGeneral:Too many requests"]}
|
|
'ETrade:User Locked': AccountSuspended, # {"error":["ETrade:User Locked"]}
|
|
},
|
|
'broad': {
|
|
':Invalid order': InvalidOrder,
|
|
':Invalid arguments:volume': InvalidOrder,
|
|
':Invalid arguments:viqc': InvalidOrder,
|
|
':Invalid nonce': InvalidNonce,
|
|
':IInsufficient funds': InsufficientFunds,
|
|
':Cancel pending': CancelPending,
|
|
':Rate limit exceeded': RateLimitExceeded,
|
|
},
|
|
},
|
|
})
|
|
|
|
def fee_to_precision(self, symbol, fee):
|
|
return self.decimal_to_precision(fee, TRUNCATE, self.markets[symbol]['precision']['amount'], self.precisionMode)
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for kraken
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTradableAssetPairs
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
promises = []
|
|
promises.append(self.publicGetAssetPairs(params))
|
|
if self.options['adjustForTimeDifference']:
|
|
promises.append(self.load_time_difference())
|
|
responses = promises
|
|
assetsResponse = responses[0]
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "ADAETH": {
|
|
# "altname": "ADAETH",
|
|
# "wsname": "ADA\/ETH",
|
|
# "aclass_base": "currency",
|
|
# "base": "ADA",
|
|
# "aclass_quote": "currency",
|
|
# "quote": "XETH",
|
|
# "lot": "unit",
|
|
# "pair_decimals": 7,
|
|
# "lot_decimals": 8,
|
|
# "lot_multiplier": 1,
|
|
# "leverage_buy": [],
|
|
# "leverage_sell": [],
|
|
# "fees": [
|
|
# [0, 0.26],
|
|
# [50000, 0.24],
|
|
# [100000, 0.22],
|
|
# [250000, 0.2],
|
|
# [500000, 0.18],
|
|
# [1000000, 0.16],
|
|
# [2500000, 0.14],
|
|
# [5000000, 0.12],
|
|
# [10000000, 0.1]
|
|
# ],
|
|
# "fees_maker": [
|
|
# [0, 0.16],
|
|
# [50000, 0.14],
|
|
# [100000, 0.12],
|
|
# [250000, 0.1],
|
|
# [500000, 0.08],
|
|
# [1000000, 0.06],
|
|
# [2500000, 0.04],
|
|
# [5000000, 0.02],
|
|
# [10000000, 0]
|
|
# ],
|
|
# "fee_volume_currency": "ZUSD",
|
|
# "margin_call": 80,
|
|
# "margin_stop": 40,
|
|
# "ordermin": "1"
|
|
# },
|
|
# }
|
|
# }
|
|
#
|
|
markets = self.safe_dict(assetsResponse, 'result', {})
|
|
cachedCurrencies = self.safe_dict(self.options, 'cachedCurrencies', {})
|
|
keys = list(markets.keys())
|
|
result = []
|
|
for i in range(0, len(keys)):
|
|
id = keys[i]
|
|
market = markets[id]
|
|
baseIdRaw = self.safe_string(market, 'base')
|
|
quoteIdRaw = self.safe_string(market, 'quote')
|
|
baseId = self.safe_currency_code(baseIdRaw)
|
|
quoteId = self.safe_currency_code(quoteIdRaw)
|
|
base = baseId
|
|
quote = quoteId
|
|
makerFees = self.safe_list(market, 'fees_maker', [])
|
|
firstMakerFee = self.safe_list(makerFees, 0, [])
|
|
firstMakerFeeRate = self.safe_string(firstMakerFee, 1)
|
|
maker = None
|
|
if firstMakerFeeRate is not None:
|
|
maker = self.parse_number(Precise.string_div(firstMakerFeeRate, '100'))
|
|
takerFees = self.safe_list(market, 'fees', [])
|
|
firstTakerFee = self.safe_list(takerFees, 0, [])
|
|
firstTakerFeeRate = self.safe_string(firstTakerFee, 1)
|
|
taker = None
|
|
if firstTakerFeeRate is not None:
|
|
taker = self.parse_number(Precise.string_div(firstTakerFeeRate, '100'))
|
|
leverageBuy = self.safe_list(market, 'leverage_buy', [])
|
|
leverageBuyLength = len(leverageBuy)
|
|
precisionPrice = self.parse_number(self.parse_precision(self.safe_string(market, 'pair_decimals')))
|
|
precisionAmount = self.parse_number(self.parse_precision(self.safe_string(market, 'lot_decimals')))
|
|
spot = True
|
|
# fix https://github.com/freqtrade/freqtrade/issues/11765#issuecomment-2894224103
|
|
if spot and (base in cachedCurrencies):
|
|
currency = cachedCurrencies[base]
|
|
currencyPrecision = self.safe_number(currency, 'precision')
|
|
# if currency precision is greater(e.g. 0.01) than market precision(e.g. 0.001)
|
|
if currencyPrecision > precisionAmount:
|
|
precisionAmount = currencyPrecision
|
|
status = self.safe_string(market, 'status')
|
|
isActive = status == 'online'
|
|
result.append({
|
|
'id': id,
|
|
'wsId': self.safe_string(market, 'wsname'),
|
|
'symbol': base + '/' + quote,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': None,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': None,
|
|
'altname': market['altname'],
|
|
'type': 'spot',
|
|
'spot': spot,
|
|
'margin': (leverageBuyLength > 0),
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'active': isActive,
|
|
'contract': False,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'taker': taker,
|
|
'maker': maker,
|
|
'contractSize': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': precisionAmount,
|
|
'price': precisionPrice,
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': self.parse_number('1'),
|
|
'max': self.safe_number(leverageBuy, leverageBuyLength - 1, 1),
|
|
},
|
|
'amount': {
|
|
'min': self.safe_number(market, 'ordermin'),
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'costmin'),
|
|
'max': None,
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
self.options['marketsByAltname'] = self.index_by(result, 'altname')
|
|
return result
|
|
|
|
def fetch_status(self, params={}):
|
|
"""
|
|
the latest known information on the availability of the exchange API
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/get-system-status/
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `status structure <https://docs.ccxt.com/#/?id=exchange-status-structure>`
|
|
"""
|
|
response = self.publicGetSystemStatus(params)
|
|
#
|
|
# {
|
|
# error: [],
|
|
# result: {status: 'online', timestamp: '2024-07-22T16:34:44Z'}
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
statusRaw = self.safe_string(result, 'status')
|
|
return {
|
|
'status': 'ok' if (statusRaw == 'online') else 'maintenance',
|
|
'updated': None,
|
|
'eta': None,
|
|
'url': None,
|
|
'info': response,
|
|
}
|
|
|
|
def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getAssetInfo
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
response = self.publicGetAssets(params)
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "ATOM": {
|
|
# "aclass": "currency",
|
|
# "altname": "ATOM",
|
|
# "collateral_value": "0.7",
|
|
# "decimals": 8,
|
|
# "display_decimals": 6,
|
|
# "margin_rate": 0.02,
|
|
# "status": "enabled",
|
|
# },
|
|
# "ATOM.S": {
|
|
# "aclass": "currency",
|
|
# "altname": "ATOM.S",
|
|
# "decimals": 8,
|
|
# "display_decimals": 6,
|
|
# "status": "enabled",
|
|
# },
|
|
# "XXBT": {
|
|
# "aclass": "currency",
|
|
# "altname": "XBT",
|
|
# "decimals": 10,
|
|
# "display_decimals": 5,
|
|
# "margin_rate": 0.01,
|
|
# "status": "enabled",
|
|
# },
|
|
# "XETH": {
|
|
# "aclass": "currency",
|
|
# "altname": "ETH",
|
|
# "decimals": 10,
|
|
# "display_decimals": 5
|
|
# "margin_rate": 0.02,
|
|
# "status": "enabled",
|
|
# },
|
|
# "XBT.M": {
|
|
# "aclass": "currency",
|
|
# "altname": "XBT.M",
|
|
# "decimals": 10,
|
|
# "display_decimals": 5
|
|
# "status": "enabled",
|
|
# },
|
|
# "ETH.M": {
|
|
# "aclass": "currency",
|
|
# "altname": "ETH.M",
|
|
# "decimals": 10,
|
|
# "display_decimals": 5
|
|
# "status": "enabled",
|
|
# },
|
|
# ...
|
|
# },
|
|
# }
|
|
#
|
|
currencies = self.safe_value(response, 'result', {})
|
|
ids = list(currencies.keys())
|
|
result: dict = {}
|
|
for i in range(0, len(ids)):
|
|
id = ids[i]
|
|
currency = currencies[id]
|
|
# todo: will need to rethink the fees
|
|
# see: https://support.kraken.com/hc/en-us/articles/201893608-What-are-the-withdrawal-fees-
|
|
# to add support for multiple withdrawal/deposit methods and
|
|
# differentiated fees for each particular method
|
|
#
|
|
# Notes about abbreviations:
|
|
# Z and X prefixes: https://support.kraken.com/hc/en-us/articles/360001206766-Bitcoin-currency-code-XBT-vs-BTC
|
|
# S and M suffixes: https://support.kraken.com/hc/en-us/articles/360039879471-What-is-Asset-S-and-Asset-M-
|
|
#
|
|
code = self.safe_currency_code(id)
|
|
# the below can not be reliable done in `safeCurrencyCode`, so we have to do it here
|
|
if id.find('.') < 0:
|
|
altName = self.safe_string(currency, 'altname')
|
|
# handle cases like below:
|
|
#
|
|
# id | altname
|
|
# ---------------
|
|
# XXBT | XBT
|
|
# ZUSD | USD
|
|
if id != altName and (id.startswith('X') or id.startswith('Z')):
|
|
code = self.safe_currency_code(altName)
|
|
# also, add map in commonCurrencies:
|
|
self.commonCurrencies[id] = code
|
|
else:
|
|
code = self.safe_currency_code(id)
|
|
isFiat = code.find('.HOLD') >= 0
|
|
result[code] = self.safe_currency_structure({
|
|
'id': id,
|
|
'code': code,
|
|
'info': currency,
|
|
'name': self.safe_string(currency, 'altname'),
|
|
'active': self.safe_string(currency, 'status') == 'enabled',
|
|
'type': 'fiat' if isFiat else 'crypto',
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': None,
|
|
'precision': self.parse_number(self.parse_precision(self.safe_string(currency, 'decimals'))),
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'networks': {},
|
|
})
|
|
return result
|
|
|
|
def safe_currency_code(self, currencyId: Str, currency: Currency = None) -> Str:
|
|
if currencyId is None:
|
|
return currencyId
|
|
if currencyId.find('.') > 0:
|
|
# if ID contains .M, .S or .F, then it can't contain X or Z prefix. in such case, ID equals to ALTNAME
|
|
parts = currencyId.split('.')
|
|
firstPart = self.safe_string(parts, 0)
|
|
secondPart = self.safe_string(parts, 1)
|
|
return super(kraken, self).safe_currency_code(firstPart, currency) + '.' + secondPart
|
|
return super(kraken, self).safe_currency_code(currencyId, currency)
|
|
|
|
def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface:
|
|
"""
|
|
fetch the trading fees for a market
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradeVolume
|
|
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `fee structure <https://docs.ccxt.com/#/?id=fee-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
'fee-info': True,
|
|
}
|
|
response = self.privatePostTradeVolume(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "currency": 'ZUSD',
|
|
# "volume": '0.0000',
|
|
# "fees": {
|
|
# "XXBTZUSD": {
|
|
# "fee": '0.2600',
|
|
# "minfee": '0.1000',
|
|
# "maxfee": '0.2600',
|
|
# "nextfee": '0.2400',
|
|
# "tiervolume": '0.0000',
|
|
# "nextvolume": '50000.0000'
|
|
# }
|
|
# },
|
|
# "fees_maker": {
|
|
# "XXBTZUSD": {
|
|
# "fee": '0.1600',
|
|
# "minfee": '0.0000',
|
|
# "maxfee": '0.1600',
|
|
# "nextfee": '0.1400',
|
|
# "tiervolume": '0.0000',
|
|
# "nextvolume": '50000.0000'
|
|
# }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_value(response, 'result', {})
|
|
return self.parse_trading_fee(result, market)
|
|
|
|
def parse_trading_fee(self, response, market):
|
|
makerFees = self.safe_value(response, 'fees_maker', {})
|
|
takerFees = self.safe_value(response, 'fees', {})
|
|
symbolMakerFee = self.safe_value(makerFees, market['id'], {})
|
|
symbolTakerFee = self.safe_value(takerFees, market['id'], {})
|
|
return {
|
|
'info': response,
|
|
'symbol': market['symbol'],
|
|
'maker': self.parse_number(Precise.string_div(self.safe_string(symbolMakerFee, 'fee'), '100')),
|
|
'taker': self.parse_number(Precise.string_div(self.safe_string(symbolTakerFee, 'fee'), '100')),
|
|
'percentage': True,
|
|
'tierBased': True,
|
|
}
|
|
|
|
def parse_bid_ask(self, bidask, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2):
|
|
price = self.safe_number(bidask, priceKey)
|
|
amount = self.safe_number(bidask, amountKey)
|
|
timestamp = self.safe_integer(bidask, 2)
|
|
return [price, amount, timestamp]
|
|
|
|
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.kraken.com/rest/#tag/Spot-Market-Data/operation/getOrderBook
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['count'] = limit # 100
|
|
response = self.publicGetDepth(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":{
|
|
# "XETHXXBT":{
|
|
# "asks":[
|
|
# ["0.023480","4.000",1586321307],
|
|
# ["0.023490","50.095",1586321306],
|
|
# ["0.023500","28.535",1586321302],
|
|
# ],
|
|
# "bids":[
|
|
# ["0.023470","59.580",1586321307],
|
|
# ["0.023460","20.000",1586321301],
|
|
# ["0.023440","67.832",1586321306],
|
|
# ]
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_value(response, 'result', {})
|
|
orderbook = self.safe_value(result, market['id'])
|
|
# sometimes kraken returns wsname instead of market id
|
|
# https://github.com/ccxt/ccxt/issues/8662
|
|
marketInfo = self.safe_value(market, 'info', {})
|
|
wsName = self.safe_value(marketInfo, 'wsname')
|
|
if wsName is not None:
|
|
orderbook = self.safe_value(result, wsName, orderbook)
|
|
return self.parse_order_book(orderbook, symbol)
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# {
|
|
# "a":["2432.77000","1","1.000"],
|
|
# "b":["2431.37000","2","2.000"],
|
|
# "c":["2430.58000","0.04408910"],
|
|
# "v":["4147.94474901","8896.96086304"],
|
|
# "p":["2456.22239","2568.63032"],
|
|
# "t":[3907,10056],
|
|
# "l":["2302.18000","2302.18000"],
|
|
# "h":["2621.14000","2860.01000"],
|
|
# "o":"2571.56000"
|
|
# }
|
|
#
|
|
symbol = self.safe_symbol(None, market)
|
|
v = self.safe_value(ticker, 'v', [])
|
|
baseVolume = self.safe_string(v, 1)
|
|
p = self.safe_value(ticker, 'p', [])
|
|
vwap = self.safe_string(p, 1)
|
|
quoteVolume = Precise.string_mul(baseVolume, vwap)
|
|
c = self.safe_value(ticker, 'c', [])
|
|
last = self.safe_string(c, 0)
|
|
high = self.safe_value(ticker, 'h', [])
|
|
low = self.safe_value(ticker, 'l', [])
|
|
bid = self.safe_value(ticker, 'b', [])
|
|
ask = self.safe_value(ticker, 'a', [])
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'high': self.safe_string(high, 1),
|
|
'low': self.safe_string(low, 1),
|
|
'bid': self.safe_string(bid, 0),
|
|
'bidVolume': self.safe_string(bid, 2),
|
|
'ask': self.safe_string(ask, 0),
|
|
'askVolume': self.safe_string(ask, 2),
|
|
'vwap': vwap,
|
|
'open': self.safe_string(ticker, 'o'),
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': baseVolume,
|
|
'quoteVolume': quoteVolume,
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation
|
|
|
|
:param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {}
|
|
if symbols is not None:
|
|
symbols = self.market_symbols(symbols)
|
|
marketIds = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.markets[symbol]
|
|
if market['active']:
|
|
marketIds.append(market['id'])
|
|
request['pair'] = ','.join(marketIds)
|
|
response = self.publicGetTicker(self.extend(request, params))
|
|
tickers = response['result']
|
|
ids = list(tickers.keys())
|
|
result: dict = {}
|
|
for i in range(0, len(ids)):
|
|
id = ids[i]
|
|
market = self.safe_market(id)
|
|
symbol = market['symbol']
|
|
ticker = tickers[id]
|
|
result[symbol] = self.parse_ticker(ticker, market)
|
|
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
|
|
|
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.kraken.com/rest/#tag/Spot-Market-Data/operation/getTickerInformation
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
response = self.publicGetTicker(self.extend(request, params))
|
|
ticker = response['result'][market['id']]
|
|
return self.parse_ticker(ticker, market)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# [
|
|
# 1591475640,
|
|
# "0.02500",
|
|
# "0.02500",
|
|
# "0.02500",
|
|
# "0.02500",
|
|
# "0.02500",
|
|
# "9.12201000",
|
|
# 5
|
|
# ]
|
|
#
|
|
return [
|
|
self.safe_timestamp(ohlcv, 0),
|
|
self.safe_number(ohlcv, 1),
|
|
self.safe_number(ohlcv, 2),
|
|
self.safe_number(ohlcv, 3),
|
|
self.safe_number(ohlcv, 4),
|
|
self.safe_number(ohlcv, 6),
|
|
]
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/get-ohlc-data
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 720)
|
|
market = self.market(symbol)
|
|
parsedTimeframe = self.safe_integer(self.timeframes, timeframe)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
if parsedTimeframe is not None:
|
|
request['interval'] = parsedTimeframe
|
|
else:
|
|
request['interval'] = timeframe
|
|
if since is not None:
|
|
scaledSince = self.parse_to_int(since / 1000)
|
|
timeFrameInSeconds = parsedTimeframe * 60
|
|
request['since'] = self.number_to_string(scaledSince - timeFrameInSeconds) # expected to be in seconds
|
|
response = self.publicGetOHLC(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":{
|
|
# "XETHXXBT":[
|
|
# [1591475580,"0.02499","0.02499","0.02499","0.02499","0.00000","0.00000000",0],
|
|
# [1591475640,"0.02500","0.02500","0.02500","0.02500","0.02500","9.12201000",5],
|
|
# [1591475700,"0.02499","0.02499","0.02499","0.02499","0.02499","1.28681415",2],
|
|
# [1591475760,"0.02499","0.02499","0.02499","0.02499","0.02499","0.08800000",1],
|
|
# ],
|
|
# "last":1591517580
|
|
# }
|
|
# }
|
|
result = self.safe_value(response, 'result', {})
|
|
ohlcvs = self.safe_list(result, market['id'], [])
|
|
return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit)
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
types: dict = {
|
|
'trade': 'trade',
|
|
'withdrawal': 'transaction',
|
|
'deposit': 'transaction',
|
|
'transfer': 'transfer',
|
|
'margin': 'margin',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
|
#
|
|
# {
|
|
# 'LTFK7F-N2CUX-PNY4SX': {
|
|
# "refid": "TSJTGT-DT7WN-GPPQMJ",
|
|
# "time": 1520102320.555,
|
|
# "type": "trade",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "amount": "0.1087194600",
|
|
# "fee": "0.0000000000",
|
|
# "balance": "0.2855851000"
|
|
# },
|
|
# ...
|
|
# }
|
|
#
|
|
id = self.safe_string(item, 'id')
|
|
direction = None
|
|
account = None
|
|
referenceId = self.safe_string(item, 'refid')
|
|
referenceAccount = None
|
|
type = self.parse_ledger_entry_type(self.safe_string(item, 'type'))
|
|
currencyId = self.safe_string(item, 'asset')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
currency = self.safe_currency(currencyId, currency)
|
|
amount = self.safe_string(item, 'amount')
|
|
if Precise.string_lt(amount, '0'):
|
|
direction = 'out'
|
|
amount = Precise.string_abs(amount)
|
|
else:
|
|
direction = 'in'
|
|
timestamp = self.safe_integer_product(item, 'time', 1000)
|
|
return self.safe_ledger_entry({
|
|
'info': item,
|
|
'id': id,
|
|
'direction': direction,
|
|
'account': account,
|
|
'referenceId': referenceId,
|
|
'referenceAccount': referenceAccount,
|
|
'type': type,
|
|
'currency': code,
|
|
'amount': self.parse_number(amount),
|
|
'before': None,
|
|
'after': self.safe_number(item, 'balance'),
|
|
'status': 'ok',
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'fee': {
|
|
'cost': self.safe_number(item, 'fee'),
|
|
'currency': code,
|
|
},
|
|
}, currency)
|
|
|
|
def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
|
|
"""
|
|
fetch the history of changes, actions done by the user or operations that altered the balance of the user
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getLedgers
|
|
|
|
:param str [code]: unified currency code, default is None
|
|
:param int [since]: timestamp in ms of the earliest ledger entry, default is None
|
|
:param int [limit]: max number of ledger entries to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest ledger entry
|
|
:param int [params.end]: timestamp in seconds of the latest ledger entry
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
|
|
"""
|
|
# https://www.kraken.com/features/api#get-ledgers-info
|
|
self.load_markets()
|
|
request: dict = {}
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['asset'] = currency['id']
|
|
if since is not None:
|
|
request['start'] = self.parse_to_int(since / 1000)
|
|
until = self.safe_string_n(params, ['until', 'till'])
|
|
if until is not None:
|
|
params = self.omit(params, ['until', 'till'])
|
|
untilDivided = Precise.string_div(until, '1000')
|
|
request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1'))
|
|
response = self.privatePostLedgers(self.extend(request, params))
|
|
# { error: [],
|
|
# "result": {ledger: {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N",
|
|
# "time": 1520103488.314,
|
|
# "type": "withdrawal",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "amount": "-0.2805800000",
|
|
# "fee": "0.0050000000",
|
|
# "balance": "0.0000051000" },
|
|
result = self.safe_value(response, 'result', {})
|
|
ledger = self.safe_value(result, 'ledger', {})
|
|
keys = list(ledger.keys())
|
|
items = []
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
value = ledger[key]
|
|
value['id'] = key
|
|
items.append(value)
|
|
return self.parse_ledger(items, currency, since, limit)
|
|
|
|
def fetch_ledger_entries_by_ids(self, ids, code: Str = None, params={}):
|
|
# https://www.kraken.com/features/api#query-ledgers
|
|
self.load_markets()
|
|
ids = ','.join(ids)
|
|
request = self.extend({
|
|
'id': ids,
|
|
}, params)
|
|
response = self.privatePostQueryLedgers(request)
|
|
# { error: [],
|
|
# "result": {'LPUAIB-TS774-UKHP7X': { refid: "A2B4HBV-L4MDIE-JU4N3N",
|
|
# "time": 1520103488.314,
|
|
# "type": "withdrawal",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "amount": "-0.2805800000",
|
|
# "fee": "0.0050000000",
|
|
# "balance": "0.0000051000" }}}
|
|
result = response['result']
|
|
keys = list(result.keys())
|
|
items = []
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
value = result[key]
|
|
value['id'] = key
|
|
items.append(value)
|
|
return self.parse_ledger(items)
|
|
|
|
def fetch_ledger_entry(self, id: str, code: Str = None, params={}) -> LedgerEntry:
|
|
items = self.fetch_ledger_entries_by_ids([id], code, params)
|
|
return items[0]
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# fetchTrades(public)
|
|
#
|
|
# [
|
|
# "0.032310", # price
|
|
# "4.28169434", # amount
|
|
# 1541390792.763, # timestamp
|
|
# "s", # sell or buy
|
|
# "l", # limit or market
|
|
# ""
|
|
# ]
|
|
#
|
|
# fetchOrderTrades(private)
|
|
#
|
|
# {
|
|
# "id": 'TIMIRG-WUNNE-RRJ6GT', # injected from outside
|
|
# "ordertxid": 'OQRPN2-LRHFY-HIFA7D',
|
|
# "postxid": 'TKH2SE-M7IF5-CFI7LT',
|
|
# "pair": 'USDCUSDT',
|
|
# "time": 1586340086.457,
|
|
# "type": 'sell',
|
|
# "ordertype": 'market',
|
|
# "price": '0.99860000',
|
|
# "cost": '22.16892001',
|
|
# "fee": '0.04433784',
|
|
# "vol": '22.20000000',
|
|
# "margin": '0.00000000',
|
|
# "misc": ''
|
|
# }
|
|
#
|
|
# fetchMyTrades
|
|
#
|
|
# {
|
|
# "ordertxid": "OSJVN7-A2AE-63WZV",
|
|
# "postxid": "TBP7O6-PNXI-CONU",
|
|
# "pair": "XXBTZUSD",
|
|
# "time": 1710429248.3052235,
|
|
# "type": "sell",
|
|
# "ordertype": "liquidation market",
|
|
# "price": "72026.50000",
|
|
# "cost": "7.20265",
|
|
# "fee": "0.01873",
|
|
# "vol": "0.00010000",
|
|
# "margin": "1.44053",
|
|
# "leverage": "5",
|
|
# "misc": "closing",
|
|
# "trade_id": 68230622,
|
|
# "maker": False
|
|
# }
|
|
#
|
|
# watchTrades
|
|
#
|
|
# {
|
|
# "symbol": "BTC/USD",
|
|
# "side": "buy",
|
|
# "price": 109601.2,
|
|
# "qty": 0.04561994,
|
|
# "ord_type": "market",
|
|
# "trade_id": 83449369,
|
|
# "timestamp": "2025-05-27T11:24:03.847761Z"
|
|
# }
|
|
#
|
|
timestamp = None
|
|
datetime = None
|
|
side = None
|
|
type = None
|
|
price = None
|
|
amount = None
|
|
id = None
|
|
orderId = None
|
|
fee = None
|
|
symbol = None
|
|
if isinstance(trade, list):
|
|
timestamp = self.safe_timestamp(trade, 2)
|
|
side = 'sell' if (trade[3] == 's') else 'buy'
|
|
type = 'limit' if (trade[4] == 'l') else 'market'
|
|
price = self.safe_string(trade, 0)
|
|
amount = self.safe_string(trade, 1)
|
|
tradeLength = len(trade)
|
|
if tradeLength > 6:
|
|
id = self.safe_string(trade, 6) # artificially added #1794
|
|
elif isinstance(trade, str):
|
|
id = trade
|
|
elif 'ordertxid' in trade:
|
|
marketId = self.safe_string(trade, 'pair')
|
|
foundMarket = self.find_market_by_altname_or_id(marketId)
|
|
if foundMarket is not None:
|
|
market = foundMarket
|
|
elif marketId is not None:
|
|
# delisted market ids go here
|
|
market = self.get_delisted_market_by_id(marketId)
|
|
orderId = self.safe_string(trade, 'ordertxid')
|
|
id = self.safe_string_2(trade, 'id', 'postxid')
|
|
timestamp = self.safe_timestamp(trade, 'time')
|
|
side = self.safe_string(trade, 'type')
|
|
type = self.safe_string(trade, 'ordertype')
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'vol')
|
|
if 'fee' in trade:
|
|
currency = None
|
|
if market is not None:
|
|
currency = market['quote']
|
|
fee = {
|
|
'cost': self.safe_string(trade, 'fee'),
|
|
'currency': currency,
|
|
}
|
|
else:
|
|
symbol = self.safe_string(trade, 'symbol')
|
|
datetime = self.safe_string(trade, 'timestamp')
|
|
id = self.safe_string(trade, 'trade_id')
|
|
side = self.safe_string(trade, 'side')
|
|
type = self.safe_string(trade, 'ord_type')
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'qty')
|
|
if market is not None:
|
|
symbol = market['symbol']
|
|
cost = self.safe_string(trade, 'cost')
|
|
maker = self.safe_bool(trade, 'maker')
|
|
takerOrMaker = None
|
|
if maker is not None:
|
|
takerOrMaker = 'maker' if maker else 'taker'
|
|
if datetime is None:
|
|
datetime = self.iso8601(timestamp)
|
|
else:
|
|
timestamp = self.parse8601(datetime)
|
|
return self.safe_trade({
|
|
'id': id,
|
|
'order': orderId,
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': datetime,
|
|
'symbol': symbol,
|
|
'type': type,
|
|
'side': side,
|
|
'takerOrMaker': takerOrMaker,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': cost,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getRecentTrades
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
id = market['id']
|
|
request: dict = {
|
|
'pair': id,
|
|
}
|
|
# https://support.kraken.com/hc/en-us/articles/218198197-How-to-pull-all-trade-data-using-the-Kraken-REST-API
|
|
# https://github.com/ccxt/ccxt/issues/5677
|
|
if since is not None:
|
|
request['since'] = self.number_to_string(self.parse_to_int(since / 1000)) # expected to be in seconds
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
response = self.publicGetTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "XETHXXBT": [
|
|
# ["0.032310","4.28169434",1541390792.763,"s","l",""]
|
|
# ],
|
|
# "last": "1541439421200678657"
|
|
# }
|
|
# }
|
|
#
|
|
result = response['result']
|
|
trades = result[id]
|
|
# trades is a sorted array: last(most recent trade) goes last
|
|
length = len(trades)
|
|
if length <= 0:
|
|
return []
|
|
lastTrade = trades[length - 1]
|
|
lastTradeId = self.safe_string(result, 'last')
|
|
lastTrade.append(lastTradeId)
|
|
trades[length - 1] = lastTrade
|
|
return self.parse_trades(trades, market, since, limit)
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
balances = self.safe_value(response, 'result', {})
|
|
result: dict = {
|
|
'info': response,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
}
|
|
currencyIds = list(balances.keys())
|
|
for i in range(0, len(currencyIds)):
|
|
currencyId = currencyIds[i]
|
|
code = self.safe_currency_code(currencyId)
|
|
balance = self.safe_value(balances, currencyId, {})
|
|
account = self.account()
|
|
account['used'] = self.safe_string(balance, 'hold_trade')
|
|
account['total'] = self.safe_string(balance, 'balance')
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
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.kraken.com/rest/#tag/Account-Data/operation/getExtendedBalance
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
self.load_markets()
|
|
response = self.privatePostBalanceEx(params)
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "ZUSD": {
|
|
# "balance": 25435.21,
|
|
# "hold_trade": 8249.76
|
|
# },
|
|
# "XXBT": {
|
|
# "balance": 1.2435,
|
|
# "hold_trade": 0.8423
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
return self.parse_balance(response)
|
|
|
|
def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}):
|
|
"""
|
|
create a market order by providing the symbol, side and cost
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder
|
|
|
|
:param str symbol: unified symbol of the market to create an order in(only USD markets are supported)
|
|
:param str side: 'buy' or 'sell'
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
# only buy orders are supported by the endpoint
|
|
req = {
|
|
'cost': cost,
|
|
}
|
|
return self.create_order(symbol, 'market', side, cost, None, self.extend(req, params))
|
|
|
|
def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}):
|
|
"""
|
|
create a market buy order by providing the symbol, side and cost
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Trading/operation/addOrder
|
|
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
return self.create_market_order_with_cost(symbol, 'buy', cost, params)
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/add-order
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param bool [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately
|
|
:param bool [params.reduceOnly]: *margin only* indicates if self order is to reduce the size of a position
|
|
:param float [params.stopLossPrice]: *margin only* the price that a stop loss order is triggered at
|
|
:param float [params.takeProfitPrice]: *margin only* the price that a take profit order is triggered at
|
|
:param str [params.trailingAmount]: *margin only* the quote amount to trail away from the current market price
|
|
:param str [params.trailingPercent]: *margin only* the percent to trail away from the current market price
|
|
:param str [params.trailingLimitAmount]: *margin only* the quote amount away from the trailingAmount
|
|
:param str [params.trailingLimitPercent]: *margin only* the percent away from the trailingAmount
|
|
:param str [params.offset]: *margin only* '+' or '-' whether you want the trailingLimitAmount value to be positive or negative, default is negative '-'
|
|
:param str [params.trigger]: *margin only* the activation price type, 'last' or 'index', default is 'last'
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
'type': side,
|
|
'ordertype': type,
|
|
'volume': self.amount_to_precision(symbol, amount),
|
|
}
|
|
orderRequest = self.order_request('createOrder', symbol, type, request, amount, price, params)
|
|
flags = self.safe_string(orderRequest[0], 'oflags', '')
|
|
isUsingCost = flags.find('viqc') > -1
|
|
response = self.privatePostAddOrder(self.extend(orderRequest[0], orderRequest[1]))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "descr": {order: 'buy 0.02100000 ETHUSDT @ limit 330.00'}, # see more examples in "parseOrder"
|
|
# "txid": ['OEKVV2-IH52O-TPL6GZ']
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
result['usingCost'] = isUsingCost
|
|
# it's impossible to know if the order was created using cost or base currency
|
|
# becuase kraken only returns something like self: {order: 'buy 10.00000000 LTCUSD @ market'}
|
|
# self usingCost flag is used to help the parsing but omited from the order
|
|
return self.parse_order(result)
|
|
|
|
def create_orders(self, orders: List[OrderRequest], params={}):
|
|
"""
|
|
create a list of trade orders
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/add-order-batch/
|
|
|
|
:param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
ordersRequests = []
|
|
orderSymbols = []
|
|
symbol = None
|
|
market = None
|
|
for i in range(0, len(orders)):
|
|
rawOrder = orders[i]
|
|
marketId = self.safe_string(rawOrder, 'symbol')
|
|
if symbol is None:
|
|
symbol = marketId
|
|
else:
|
|
if symbol != marketId:
|
|
raise BadRequest(self.id + ' createOrders() requires all orders to have the same symbol')
|
|
market = self.market(marketId)
|
|
orderSymbols.append(marketId)
|
|
type = self.safe_string(rawOrder, 'type')
|
|
side = self.safe_string(rawOrder, 'side')
|
|
amount = self.safe_value(rawOrder, 'amount')
|
|
price = self.safe_value(rawOrder, 'price')
|
|
orderParams = self.safe_dict(rawOrder, 'params', {})
|
|
req: dict = {
|
|
'type': side,
|
|
'ordertype': type,
|
|
'volume': self.amount_to_precision(market['symbol'], amount),
|
|
}
|
|
orderRequest = self.order_request('createOrders', marketId, type, req, amount, price, orderParams)
|
|
ordersRequests.append(orderRequest[0])
|
|
orderSymbols = self.market_symbols(orderSymbols, None, False, True, True)
|
|
response = None
|
|
request: dict = {
|
|
'orders': ordersRequests,
|
|
'pair': market['id'],
|
|
}
|
|
request = self.extend(request, params)
|
|
response = self.privatePostAddOrderBatch(request)
|
|
#
|
|
# {
|
|
# "error":[
|
|
# ],
|
|
# "result":{
|
|
# "orders":[
|
|
# {
|
|
# "txid":"OEPPJX-34RMM-OROGZE",
|
|
# "descr":{
|
|
# "order":"sell 6.000000 ADAUSDC @ limit 0.400000"
|
|
# }
|
|
# },
|
|
# {
|
|
# "txid":"OLQY7O-OYBXW-W23PGL",
|
|
# "descr":{
|
|
# "order":"sell 6.000000 ADAUSDC @ limit 0.400000"
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_orders(self.safe_list(result, 'orders'))
|
|
|
|
def find_market_by_altname_or_id(self, id):
|
|
marketsByAltname = self.safe_value(self.options, 'marketsByAltname', {})
|
|
if id in marketsByAltname:
|
|
return marketsByAltname[id]
|
|
else:
|
|
return self.safe_market(id)
|
|
|
|
def get_delisted_market_by_id(self, id):
|
|
if id is None:
|
|
return id
|
|
market = self.safe_value(self.options['delistedMarketsById'], id)
|
|
if market is not None:
|
|
return market
|
|
baseIdStart = 0
|
|
baseIdEnd = 3
|
|
quoteIdStart = 3
|
|
quoteIdEnd = 6
|
|
if len(id) == 8:
|
|
baseIdEnd = 4
|
|
quoteIdStart = 4
|
|
quoteIdEnd = 8
|
|
elif len(id) == 7:
|
|
baseIdEnd = 4
|
|
quoteIdStart = 4
|
|
quoteIdEnd = 7
|
|
baseId = id[baseIdStart:baseIdEnd]
|
|
quoteId = id[quoteIdStart:quoteIdEnd]
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
symbol = base + '/' + quote
|
|
market = {
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
}
|
|
self.options['delistedMarketsById'][id] = market
|
|
return market
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'pending': 'open', # order pending book entry
|
|
'open': 'open',
|
|
'pending_new': 'open',
|
|
'new': 'open',
|
|
'partially_filled': 'open',
|
|
'filled': 'closed',
|
|
'closed': 'closed',
|
|
'canceled': 'canceled',
|
|
'expired': 'expired',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order_type(self, status):
|
|
statuses: dict = {
|
|
# we dont add "space" delimited orders here(eg. stop loss) because they need separate parsing
|
|
'take-profit': 'market',
|
|
'stop-loss': 'market',
|
|
'stop-loss-limit': 'limit',
|
|
'take-profit-limit': 'limit',
|
|
'trailing-stop-limit': 'limit',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# createOrder
|
|
#
|
|
# {
|
|
# "descr": {
|
|
# "order": "buy 0.02100000 ETHUSDT @ limit 330.00" # limit orders
|
|
# "buy 0.12345678 ETHUSDT @ market" # market order
|
|
# "sell 0.28002676 ETHUSDT @ stop loss 0.0123 -> limit 0.0.1222" # stop order
|
|
# "sell 0.00100000 ETHUSDT @ stop loss 2677.00 -> limit 2577.00 with 5:1 leverage"
|
|
# "buy 0.10000000 LTCUSDT @ take profit 75.00000 -> limit 74.00000"
|
|
# "sell 10.00000000 XRPEUR @ trailing stop +50.0000%" # trailing stop
|
|
# },
|
|
# "txid": ['OEKVV2-IH52O-TPL6GZ']
|
|
# }
|
|
#
|
|
# editOrder
|
|
#
|
|
# {
|
|
# "amend_id": "TJSMEH-AA67V-YUSQ6O"
|
|
# }
|
|
#
|
|
# ws - createOrder
|
|
# {
|
|
# "order_id": "OXM2QD-EALR2-YBAVEU"
|
|
# }
|
|
#
|
|
# ws - editOrder
|
|
# {
|
|
# "amend_id": "TJSMEH-AA67V-YUSQ6O",
|
|
# "order_id": "OXM2QD-EALR2-YBAVEU"
|
|
# }
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "open": {
|
|
# "OXVPSU-Q726F-L3SDEP": {
|
|
# "refid": null,
|
|
# "userref": 0,
|
|
# "status": "open",
|
|
# "opentm": 1706893367.4656649,
|
|
# "starttm": 0,
|
|
# "expiretm": 0,
|
|
# "descr": {
|
|
# "pair": "XRPEUR",
|
|
# "type": "sell",
|
|
# "ordertype": "trailing-stop",
|
|
# "price": "+50.0000%",
|
|
# "price2": "0",
|
|
# "leverage": "none",
|
|
# "order": "sell 10.00000000 XRPEUR @ trailing stop +50.0000%",
|
|
# "close": ""
|
|
# },
|
|
# "vol": "10.00000000",
|
|
# "vol_exec": "0.00000000",
|
|
# "cost": "0.00000000",
|
|
# "fee": "0.00000000",
|
|
# "price": "0.00000000",
|
|
# "stopprice": "0.23424000",
|
|
# "limitprice": "0.46847000",
|
|
# "misc": "",
|
|
# "oflags": "fciq",
|
|
# "trigger": "index"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# fetchOpenOrders
|
|
#
|
|
# {
|
|
# "refid": null,
|
|
# "userref": null,
|
|
# "cl_ord_id": "1234",
|
|
# "status": "open",
|
|
# "opentm": 1733815269.370054,
|
|
# "starttm": 0,
|
|
# "expiretm": 0,
|
|
# "descr": {
|
|
# "pair": "XBTUSD",
|
|
# "type": "buy",
|
|
# "ordertype": "limit",
|
|
# "price": "70000.0",
|
|
# "price2": "0",
|
|
# "leverage": "none",
|
|
# "order": "buy 0.00010000 XBTUSD @ limit 70000.0",
|
|
# "close": ""
|
|
# },
|
|
# "vol": "0.00010000",
|
|
# "vol_exec": "0.00000000",
|
|
# "cost": "0.00000",
|
|
# "fee": "0.00000",
|
|
# "price": "0.00000",
|
|
# "stopprice": "0.00000",
|
|
# "limitprice": "0.00000",
|
|
# "misc": "",
|
|
# "oflags": "fciq"
|
|
# }
|
|
#
|
|
isUsingCost = self.safe_bool(order, 'usingCost', False)
|
|
order = self.omit(order, 'usingCost')
|
|
description = self.safe_dict(order, 'descr', {})
|
|
orderDescriptionObj = self.safe_dict(order, 'descr') # can be null
|
|
orderDescription = None
|
|
if orderDescriptionObj is not None:
|
|
orderDescription = self.safe_string(orderDescriptionObj, 'order')
|
|
else:
|
|
orderDescription = self.safe_string(order, 'descr')
|
|
side = None
|
|
rawType = None
|
|
marketId = None
|
|
price = None
|
|
amount = None
|
|
cost = None
|
|
triggerPrice = None
|
|
if orderDescription is not None:
|
|
parts = orderDescription.split(' ')
|
|
side = self.safe_string(parts, 0)
|
|
if not isUsingCost:
|
|
amount = self.safe_string(parts, 1)
|
|
else:
|
|
cost = self.safe_string(parts, 1)
|
|
marketId = self.safe_string(parts, 2)
|
|
part4 = self.safe_string(parts, 4)
|
|
part5 = self.safe_string(parts, 5)
|
|
if part4 == 'limit' or part4 == 'market':
|
|
rawType = part4 # eg, limit, market
|
|
else:
|
|
rawType = part4 + ' ' + part5 # eg. stop loss, take profit, trailing stop
|
|
if rawType == 'stop loss' or rawType == 'take profit':
|
|
triggerPrice = self.safe_string(parts, 6)
|
|
price = self.safe_string(parts, 9)
|
|
elif rawType == 'limit':
|
|
price = self.safe_string(parts, 5)
|
|
side = self.safe_string(description, 'type', side)
|
|
rawType = self.safe_string(description, 'ordertype', rawType) # orderType has dash, e.g. trailing-stop
|
|
marketId = self.safe_string(description, 'pair', marketId)
|
|
foundMarket = self.find_market_by_altname_or_id(marketId)
|
|
symbol = None
|
|
if foundMarket is not None:
|
|
market = foundMarket
|
|
elif marketId is not None:
|
|
# delisted market ids go here
|
|
market = self.get_delisted_market_by_id(marketId)
|
|
timestamp = self.safe_timestamp(order, 'opentm')
|
|
amount = self.safe_string(order, 'vol', amount)
|
|
filled = self.safe_string(order, 'vol_exec')
|
|
fee = None
|
|
# kraken truncates the cost in the api response so we will ignore it and calculate it from average & filled
|
|
# cost = self.safe_string(order, 'cost')
|
|
price = self.safe_string(description, 'price', price)
|
|
# when type = trailing stop returns price = '+50.0000%'
|
|
if (price is not None) and (price.endswith('%') or Precise.string_equals(price, '0.00000') or Precise.string_equals(price, '0')):
|
|
price = None # self is not the price we want
|
|
if price is None:
|
|
price = self.safe_string(description, 'price2')
|
|
price = self.safe_string_2(order, 'limitprice', 'price', price)
|
|
flags = self.safe_string(order, 'oflags', '')
|
|
isPostOnly = flags.find('post') > -1
|
|
average = self.safe_number(order, 'price')
|
|
if market is not None:
|
|
symbol = market['symbol']
|
|
if 'fee' in order:
|
|
feeCost = self.safe_string(order, 'fee')
|
|
fee = {
|
|
'cost': feeCost,
|
|
'rate': None,
|
|
}
|
|
if flags.find('fciq') >= 0:
|
|
fee['currency'] = market['quote']
|
|
elif flags.find('fcib') >= 0:
|
|
fee['currency'] = market['base']
|
|
status = self.parse_order_status(self.safe_string(order, 'status'))
|
|
id = self.safe_string_n(order, ['id', 'txid', 'order_id', 'amend_id'])
|
|
if (id is None) or (id.startswith('[')):
|
|
txid = self.safe_list(order, 'txid')
|
|
id = self.safe_string(txid, 0)
|
|
userref = self.safe_string(order, 'userref')
|
|
clientOrderId = self.safe_string(order, 'cl_ord_id', userref)
|
|
rawTrades = self.safe_value(order, 'trades', [])
|
|
trades = []
|
|
for i in range(0, len(rawTrades)):
|
|
rawTrade = rawTrades[i]
|
|
if isinstance(rawTrade, str):
|
|
trades.append(self.safe_trade({'id': rawTrade, 'orderId': id, 'symbol': symbol, 'info': {}}))
|
|
else:
|
|
trades.append(rawTrade)
|
|
# in #24192 PR, self field is not something consistent/actual
|
|
# triggerPrice = self.omit_zero(self.safe_string(order, 'stopprice', triggerPrice))
|
|
stopLossPrice = None
|
|
takeProfitPrice = None
|
|
# the dashed strings are not provided from fields(eg. fetch order)
|
|
# while spaced strings from "order" sentence(when other fields not available)
|
|
if rawType is not None:
|
|
if rawType.startswith('take-profit'):
|
|
takeProfitPrice = self.safe_string(description, 'price')
|
|
price = self.omit_zero(self.safe_string(description, 'price2'))
|
|
elif rawType.startswith('stop-loss'):
|
|
stopLossPrice = self.safe_string(description, 'price')
|
|
price = self.omit_zero(self.safe_string(description, 'price2'))
|
|
elif rawType == 'take profit':
|
|
takeProfitPrice = triggerPrice
|
|
elif rawType == 'stop loss':
|
|
stopLossPrice = triggerPrice
|
|
finalType = self.parse_order_type(rawType)
|
|
# unlike from endpoints which provide eg: "take-profit-limit"
|
|
# for "space-delimited" orders we dont have market/limit suffixes, their format is
|
|
# eg: `stop loss > limit 123`, so we need to parse them manually
|
|
if self.in_array(finalType, ['stop loss', 'take profit']):
|
|
finalType = 'market' if (price is None) else 'limit'
|
|
amendId = self.safe_string(order, 'amend_id')
|
|
if amendId is not None:
|
|
isPostOnly = None
|
|
return self.safe_order({
|
|
'id': id,
|
|
'clientOrderId': clientOrderId,
|
|
'info': order,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'status': status,
|
|
'symbol': symbol,
|
|
'type': finalType,
|
|
'timeInForce': None,
|
|
'postOnly': isPostOnly,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': triggerPrice,
|
|
'takeProfitPrice': takeProfitPrice,
|
|
'stopLossPrice': stopLossPrice,
|
|
'cost': cost,
|
|
'amount': amount,
|
|
'filled': filled,
|
|
'average': average,
|
|
'remaining': None,
|
|
'reduceOnly': self.safe_bool_2(order, 'reduceOnly', 'reduce_only'),
|
|
'fee': fee,
|
|
'trades': trades,
|
|
}, market)
|
|
|
|
def order_request(self, method: str, symbol: str, type: str, request: dict, amount: Num, price: Num = None, params={}):
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
params = self.omit(params, ['clientOrderId'])
|
|
if clientOrderId is not None:
|
|
request['cl_ord_id'] = clientOrderId
|
|
stopLossTriggerPrice = self.safe_string(params, 'stopLossPrice')
|
|
takeProfitTriggerPrice = self.safe_string(params, 'takeProfitPrice')
|
|
isStopLossTriggerOrder = stopLossTriggerPrice is not None
|
|
isTakeProfitTriggerOrder = takeProfitTriggerPrice is not None
|
|
isStopLossOrTakeProfitTrigger = isStopLossTriggerOrder or isTakeProfitTriggerOrder
|
|
trailingAmount = self.safe_string(params, 'trailingAmount')
|
|
trailingPercent = self.safe_string(params, 'trailingPercent')
|
|
trailingLimitAmount = self.safe_string(params, 'trailingLimitAmount')
|
|
trailingLimitPercent = self.safe_string(params, 'trailingLimitPercent')
|
|
isTrailingAmountOrder = trailingAmount is not None
|
|
isTrailingPercentOrder = trailingPercent is not None
|
|
isLimitOrder = type.endswith('limit') # supporting limit, stop-loss-limit, take-profit-limit, etc
|
|
isMarketOrder = type == 'market'
|
|
cost = self.safe_string(params, 'cost')
|
|
flags = self.safe_string(params, 'oflags')
|
|
params = self.omit(params, ['cost', 'oflags'])
|
|
isViqcOrder = (flags is not None) and (flags.find('viqc') > -1) # volume in quote currency
|
|
if isMarketOrder and (cost is not None or isViqcOrder):
|
|
if cost is None and (amount is not None):
|
|
request['volume'] = self.cost_to_precision(symbol, self.number_to_string(amount))
|
|
else:
|
|
request['volume'] = self.cost_to_precision(symbol, cost)
|
|
extendedOflags = flags + ',viqc' if (flags is not None) else 'viqc'
|
|
request['oflags'] = extendedOflags
|
|
elif isLimitOrder and not isTrailingAmountOrder and not isTrailingPercentOrder:
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only')
|
|
if isStopLossOrTakeProfitTrigger:
|
|
if isStopLossTriggerOrder:
|
|
request['price'] = self.price_to_precision(symbol, stopLossTriggerPrice)
|
|
if isLimitOrder:
|
|
request['ordertype'] = 'stop-loss-limit'
|
|
else:
|
|
request['ordertype'] = 'stop-loss'
|
|
elif isTakeProfitTriggerOrder:
|
|
request['price'] = self.price_to_precision(symbol, takeProfitTriggerPrice)
|
|
if isLimitOrder:
|
|
request['ordertype'] = 'take-profit-limit'
|
|
else:
|
|
request['ordertype'] = 'take-profit'
|
|
if isLimitOrder:
|
|
request['price2'] = self.price_to_precision(symbol, price)
|
|
elif isTrailingAmountOrder or isTrailingPercentOrder:
|
|
trailingPercentString = None
|
|
if trailingPercent is not None:
|
|
trailingPercentString = ('+' + trailingPercent) if (trailingPercent.endswith('%')) else ('+' + trailingPercent + '%')
|
|
trailingAmountString = '+' + trailingAmount if (trailingAmount is not None) else None # must use + for self
|
|
offset = self.safe_string(params, 'offset', '-') # can use + or - for self
|
|
trailingLimitAmountString = offset + self.number_to_string(trailingLimitAmount) if (trailingLimitAmount is not None) else None
|
|
trailingActivationPriceType = self.safe_string(params, 'trigger', 'last')
|
|
request['trigger'] = trailingActivationPriceType
|
|
if isLimitOrder or (trailingLimitAmount is not None) or (trailingLimitPercent is not None):
|
|
request['ordertype'] = 'trailing-stop-limit'
|
|
if trailingLimitPercent is not None:
|
|
trailingLimitPercentString = (offset + trailingLimitPercent) if (trailingLimitPercent.endswith('%')) else (offset + trailingLimitPercent + '%')
|
|
request['price'] = trailingPercentString
|
|
request['price2'] = trailingLimitPercentString
|
|
elif trailingLimitAmount is not None:
|
|
request['price'] = trailingAmountString
|
|
request['price2'] = trailingLimitAmountString
|
|
else:
|
|
request['ordertype'] = 'trailing-stop'
|
|
if trailingPercent is not None:
|
|
request['price'] = trailingPercentString
|
|
else:
|
|
request['price'] = trailingAmountString
|
|
if reduceOnly:
|
|
if method == 'createOrderWs':
|
|
request['reduce_only'] = True # ws request can't have stringified bool
|
|
else:
|
|
request['reduce_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string
|
|
close = self.safe_dict(params, 'close')
|
|
if close is not None:
|
|
close = self.extend({}, close)
|
|
closePrice = self.safe_value(close, 'price')
|
|
if closePrice is not None:
|
|
close['price'] = self.price_to_precision(symbol, closePrice)
|
|
closePrice2 = self.safe_value(close, 'price2') # stopPrice
|
|
if closePrice2 is not None:
|
|
close['price2'] = self.price_to_precision(symbol, closePrice2)
|
|
request['close'] = close
|
|
timeInForce = self.safe_string_2(params, 'timeInForce', 'timeinforce')
|
|
if timeInForce is not None:
|
|
request['timeinforce'] = timeInForce
|
|
isMarket = (type == 'market')
|
|
postOnly = None
|
|
postOnly, params = self.handle_post_only(isMarket, False, params)
|
|
if postOnly:
|
|
extendedPostFlags = flags + ',post' if (flags is not None) else 'post'
|
|
request['oflags'] = extendedPostFlags
|
|
if (flags is not None) and not ('oflags' in request):
|
|
request['oflags'] = flags
|
|
params = self.omit(params, ['timeInForce', 'reduceOnly', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset'])
|
|
return [request, params]
|
|
|
|
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.kraken.com/api/docs/rest-api/amend-order
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float [amount]: how much of the currency you want to trade in units of the base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param float [params.stopLossPrice]: the price that a stop loss order is triggered at
|
|
:param float [params.takeProfitPrice]: the price that a take profit order is triggered at
|
|
:param str [params.trailingAmount]: the quote amount to trail away from the current market price
|
|
:param str [params.trailingPercent]: the percent to trail away from the current market price
|
|
:param str [params.trailingLimitAmount]: the quote amount away from the trailingAmount
|
|
:param str [params.trailingLimitPercent]: the percent away from the trailingAmount
|
|
:param str [params.offset]: '+' or '-' whether you want the trailingLimitAmount value to be positive or negative
|
|
:param boolean [params.postOnly]: if True, the order will only be posted to the order book and not executed immediately
|
|
:param str [params.clientOrderId]: the orders client order id
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
if not market['spot']:
|
|
raise NotSupported(self.id + ' editOrder() does not support ' + market['type'] + ' orders, only spot orders are accepted')
|
|
request: dict = {
|
|
'txid': id,
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id')
|
|
if clientOrderId is not None:
|
|
request['cl_ord_id'] = clientOrderId
|
|
params = self.omit(params, ['clientOrderId', 'cl_ord_id'])
|
|
request = self.omit(request, 'txid')
|
|
isMarket = (type == 'market')
|
|
postOnly = None
|
|
postOnly, params = self.handle_post_only(isMarket, False, params)
|
|
if postOnly:
|
|
request['post_only'] = 'true' # not using hasattr(self, boolean) case, because the urlencodedNested transforms it into 'True' string
|
|
if amount is not None:
|
|
request['order_qty'] = self.amount_to_precision(symbol, amount)
|
|
if price is not None:
|
|
request['limit_price'] = self.price_to_precision(symbol, price)
|
|
allTriggerPrices = self.safe_string_n(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent'])
|
|
if allTriggerPrices is not None:
|
|
offset = self.safe_string(params, 'offset')
|
|
params = self.omit(params, ['stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent', 'offset'])
|
|
if offset is not None:
|
|
allTriggerPrices = offset + allTriggerPrices
|
|
request['trigger_price'] = allTriggerPrices
|
|
else:
|
|
request['trigger_price'] = self.price_to_precision(symbol, allTriggerPrices)
|
|
response = self.privatePostAmendOrder(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "amend_id": "TJSMEH-AA67V-YUSQ6O"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_order(result, market)
|
|
|
|
def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getOrdersInfo
|
|
|
|
:param str id: order id
|
|
:param str symbol: not used by kraken fetchOrder
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
clientOrderId = self.safe_value_2(params, 'userref', 'clientOrderId')
|
|
request: dict = {
|
|
'trades': True, # whether or not to include trades in output(optional, default False)
|
|
'txid': id, # do not comma separate a list of ids - use fetchOrdersByIds instead
|
|
# 'userref': 'optional', # restrict results to given user reference id(optional)
|
|
}
|
|
query = params
|
|
if clientOrderId is not None:
|
|
request['userref'] = clientOrderId
|
|
query = self.omit(params, ['userref', 'clientOrderId'])
|
|
response = self.privatePostQueryOrders(self.extend(request, query))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":{
|
|
# "OTLAS3-RRHUF-NDWH5A":{
|
|
# "refid":null,
|
|
# "userref":null,
|
|
# "status":"closed",
|
|
# "reason":null,
|
|
# "opentm":1586822919.3342,
|
|
# "closetm":1586822919.365,
|
|
# "starttm":0,
|
|
# "expiretm":0,
|
|
# "descr":{
|
|
# "pair":"XBTUSDT",
|
|
# "type":"sell",
|
|
# "ordertype":"market",
|
|
# "price":"0",
|
|
# "price2":"0",
|
|
# "leverage":"none",
|
|
# "order":"sell 0.21804000 XBTUSDT @ market",
|
|
# "close":""
|
|
# },
|
|
# "vol":"0.21804000",
|
|
# "vol_exec":"0.21804000",
|
|
# "cost":"1493.9",
|
|
# "fee":"3.8",
|
|
# "price":"6851.5",
|
|
# "stopprice":"0.00000",
|
|
# "limitprice":"0.00000",
|
|
# "misc":"",
|
|
# "oflags":"fciq",
|
|
# "trades":["TT5UC3-GOIRW-6AZZ6R"]
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_value(response, 'result', [])
|
|
if not (id in result):
|
|
raise OrderNotFound(self.id + ' fetchOrder() could not find order id ' + id)
|
|
return self.parse_order(self.extend({'id': id}, result[id]))
|
|
|
|
def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all the trades made from a single order
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getTradesInfo
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
orderTrades = self.safe_value(params, 'trades')
|
|
tradeIds = []
|
|
if orderTrades is None:
|
|
raise ArgumentsRequired(self.id + " fetchOrderTrades() requires a unified order structure in the params argument or a 'trades' param(an array of trade id strings)")
|
|
else:
|
|
for i in range(0, len(orderTrades)):
|
|
orderTrade = orderTrades[i]
|
|
if isinstance(orderTrade, str):
|
|
tradeIds.append(orderTrade)
|
|
else:
|
|
tradeIds.append(orderTrade['id'])
|
|
self.load_markets()
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
options = self.safe_value(self.options, 'fetchOrderTrades', {})
|
|
batchSize = self.safe_integer(options, 'batchSize', 20)
|
|
numTradeIds = len(tradeIds)
|
|
numBatches = self.parse_to_int(numTradeIds / batchSize)
|
|
numBatches = self.sum(numBatches, 1)
|
|
result = []
|
|
for j in range(0, numBatches):
|
|
requestIds = []
|
|
for k in range(0, batchSize):
|
|
index = self.sum(j * batchSize, k)
|
|
if index < numTradeIds:
|
|
requestIds.append(tradeIds[index])
|
|
request: dict = {
|
|
'txid': ','.join(requestIds),
|
|
}
|
|
response = self.privatePostQueryTrades(request)
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# 'TIMIRG-WUNNE-RRJ6GT': {
|
|
# "ordertxid": 'OQRPN2-LRHFY-HIFA7D',
|
|
# "postxid": 'TKH2SE-M7IF5-CFI7LT',
|
|
# "pair": 'USDCUSDT',
|
|
# "time": 1586340086.457,
|
|
# "type": 'sell',
|
|
# "ordertype": 'market',
|
|
# "price": '0.99860000',
|
|
# "cost": '22.16892001',
|
|
# "fee": '0.04433784',
|
|
# "vol": '22.20000000',
|
|
# "margin": '0.00000000',
|
|
# "misc": ''
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
rawTrades = self.safe_value(response, 'result')
|
|
ids = list(rawTrades.keys())
|
|
for i in range(0, len(ids)):
|
|
rawTrades[ids[i]]['id'] = ids[i]
|
|
trades = self.parse_trades(rawTrades, None, since, limit)
|
|
tradesFilteredBySymbol = self.filter_by_symbol(trades, symbol)
|
|
result = self.array_concat(result, tradesFilteredBySymbol)
|
|
return result
|
|
|
|
def fetch_orders_by_ids(self, ids, symbol: Str = None, params={}):
|
|
"""
|
|
fetch orders by the list of order id
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getClosedOrders
|
|
|
|
:param str[] [ids]: list of order id
|
|
:param str [symbol]: unified ccxt market symbol
|
|
:param dict [params]: extra parameters specific to the kraken api endpoint
|
|
:returns dict[]: a list of `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
response = self.privatePostQueryOrders(self.extend({
|
|
'trades': True, # whether or not to include trades in output(optional, default False)
|
|
'txid': ','.join(ids), # comma delimited list of transaction ids to query info about(20 maximum)
|
|
}, params))
|
|
result = self.safe_value(response, 'result', {})
|
|
orders = []
|
|
orderIds = list(result.keys())
|
|
for i in range(0, len(orderIds)):
|
|
id = orderIds[i]
|
|
item = result[id]
|
|
order = self.parse_order(self.extend({'id': id}, item))
|
|
orders.append(order)
|
|
return orders
|
|
|
|
def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/get-trade-history
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest trade entry
|
|
:param int [params.end]: timestamp in seconds of the latest trade entry
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
# 'type': 'all', # any position, closed position, closing position, no position
|
|
# 'trades': False, # whether or not to include trades related to position in output
|
|
# 'start': 1234567890, # starting unix timestamp or trade tx id of results(exclusive)
|
|
# 'end': 1234567890, # ending unix timestamp or trade tx id of results(inclusive)
|
|
# 'ofs' = result offset
|
|
}
|
|
if since is not None:
|
|
request['start'] = self.parse_to_int(since / 1000)
|
|
until = self.safe_string_n(params, ['until', 'till'])
|
|
if until is not None:
|
|
params = self.omit(params, ['until', 'till'])
|
|
untilDivided = Precise.string_div(until, '1000')
|
|
request['end'] = self.parse_to_int(Precise.string_add(untilDivided, '1'))
|
|
response = self.privatePostTradesHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "trades": {
|
|
# "GJ3NYQ-XJRTF-THZABF": {
|
|
# "ordertxid": "TKH2SE-ZIF5E-CFI7LT",
|
|
# "postxid": "OEN3VX-M7IF5-JNBJAM",
|
|
# "pair": "XICNXETH",
|
|
# "time": 1527213229.4491,
|
|
# "type": "sell",
|
|
# "ordertype": "limit",
|
|
# "price": "0.001612",
|
|
# "cost": "0.025792",
|
|
# "fee": "0.000026",
|
|
# "vol": "16.00000000",
|
|
# "margin": "0.000000",
|
|
# "leverage": "5",
|
|
# "misc": ""
|
|
# "trade_id": 68230622,
|
|
# "maker": False
|
|
# },
|
|
# ...
|
|
# },
|
|
# "count": 9760,
|
|
# },
|
|
# }
|
|
#
|
|
trades = response['result']['trades']
|
|
ids = list(trades.keys())
|
|
for i in range(0, len(ids)):
|
|
trades[ids[i]]['id'] = ids[i]
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
return self.parse_trades(trades, market, since, limit)
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/cancel-order
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: the orders client order id
|
|
:param int [params.userref]: the orders user reference id
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
response = None
|
|
requestId = self.safe_value(params, 'userref', id) # string or integer
|
|
params = self.omit(params, 'userref')
|
|
request: dict = {
|
|
'txid': requestId, # order id or userref
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'cl_ord_id')
|
|
if clientOrderId is not None:
|
|
request['cl_ord_id'] = clientOrderId
|
|
params = self.omit(params, ['clientOrderId', 'cl_ord_id'])
|
|
request = self.omit(request, 'txid')
|
|
try:
|
|
response = self.privatePostCancelOrder(self.extend(request, params))
|
|
#
|
|
# {
|
|
# error: [],
|
|
# result: {
|
|
# count: '1'
|
|
# }
|
|
# }
|
|
#
|
|
except Exception as e:
|
|
if self.last_http_response:
|
|
if self.last_http_response.find('EOrder:Unknown order') >= 0:
|
|
raise OrderNotFound(self.id + ' cancelOrder() error ' + self.last_http_response)
|
|
raise e
|
|
return self.safe_order({
|
|
'info': response,
|
|
})
|
|
|
|
def cancel_orders(self, ids: List[str], symbol: Str = None, params={}):
|
|
"""
|
|
cancel multiple orders
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelOrderBatch
|
|
|
|
:param str[] ids: open orders transaction ID(txid) or user reference(userref)
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'orders': ids,
|
|
}
|
|
response = self.privatePostCancelOrderBatch(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "count": 2
|
|
# }
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_order({
|
|
'info': response,
|
|
}),
|
|
]
|
|
|
|
def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrders
|
|
|
|
:param str symbol: unified market symbol, not used by kraken cancelAllOrders(all open orders are cancelled)
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
response = self.privatePostCancelAll(params)
|
|
#
|
|
# {
|
|
# error: [],
|
|
# result: {
|
|
# count: '1'
|
|
# }
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_order({
|
|
'info': response,
|
|
}),
|
|
]
|
|
|
|
def cancel_all_orders_after(self, timeout: Int, params={}):
|
|
"""
|
|
dead man's switch, cancel all orders after the given timeout
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Trading/operation/cancelAllOrdersAfter
|
|
|
|
:param number timeout: time in milliseconds, 0 represents cancel the timer
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: the api result
|
|
"""
|
|
if timeout > 86400000:
|
|
raise BadRequest(self.id + ' cancelAllOrdersAfter timeout should be less than 86400000 milliseconds')
|
|
self.load_markets()
|
|
request: dict = {
|
|
'timeout': (self.parse_to_int(timeout / 1000)) if (timeout > 0) else 0,
|
|
}
|
|
response = self.privatePostCancelAllOrdersAfter(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "currentTime": "2023-03-24T17:41:56Z",
|
|
# "triggerTime": "2023-03-24T17:42:56Z"
|
|
# }
|
|
# }
|
|
#
|
|
return response
|
|
|
|
def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://docs.kraken.com/api/docs/rest-api/get-open-orders
|
|
|
|
:param str [symbol]: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: the orders client order id
|
|
:param int [params.userref]: the orders user reference id
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {}
|
|
if since is not None:
|
|
request['start'] = self.parse_to_int(since / 1000)
|
|
userref = self.safe_integer(params, 'userref')
|
|
if userref is not None:
|
|
request['userref'] = userref
|
|
params = self.omit(params, 'userref')
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
request['cl_ord_id'] = clientOrderId
|
|
params = self.omit(params, 'clientOrderId')
|
|
response = self.privatePostOpenOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "open": {
|
|
# "O45M52-BFD5S-YXKQOU": {
|
|
# "refid": null,
|
|
# "userref": null,
|
|
# "cl_ord_id": "1234",
|
|
# "status": "open",
|
|
# "opentm": 1733815269.370054,
|
|
# "starttm": 0,
|
|
# "expiretm": 0,
|
|
# "descr": {
|
|
# "pair": "XBTUSD",
|
|
# "type": "buy",
|
|
# "ordertype": "limit",
|
|
# "price": "70000.0",
|
|
# "price2": "0",
|
|
# "leverage": "none",
|
|
# "order": "buy 0.00010000 XBTUSD @ limit 70000.0",
|
|
# "close": ""
|
|
# },
|
|
# "vol": "0.00010000",
|
|
# "vol_exec": "0.00000000",
|
|
# "cost": "0.00000",
|
|
# "fee": "0.00000",
|
|
# "price": "0.00000",
|
|
# "stopprice": "0.00000",
|
|
# "limitprice": "0.00000",
|
|
# "misc": "",
|
|
# "oflags": "fciq"
|
|
# }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
result = self.safe_dict(response, 'result', {})
|
|
open = self.safe_dict(result, 'open', {})
|
|
orders = []
|
|
orderIds = list(open.keys())
|
|
for i in range(0, len(orderIds)):
|
|
id = orderIds[i]
|
|
item = open[id]
|
|
orders.append(self.extend({'id': id}, item))
|
|
return self.parse_orders(orders, market, since, limit)
|
|
|
|
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.kraken.com/api/docs/rest-api/get-closed-orders
|
|
|
|
:param str [symbol]: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest entry
|
|
:param str [params.clientOrderId]: the orders client order id
|
|
:param int [params.userref]: the orders user reference id
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {}
|
|
if since is not None:
|
|
request['start'] = self.parse_to_int(since / 1000)
|
|
userref = self.safe_integer(params, 'userref')
|
|
if userref is not None:
|
|
request['userref'] = userref
|
|
params = self.omit(params, 'userref')
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
request['cl_ord_id'] = clientOrderId
|
|
params = self.omit(params, 'clientOrderId')
|
|
request, params = self.handle_until_option('end', request, params)
|
|
response = self.privatePostClosedOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":{
|
|
# "closed":{
|
|
# "OETZYO-UL524-QJMXCT":{
|
|
# "refid":null,
|
|
# "userref":null,
|
|
# "status":"canceled",
|
|
# "reason":"User requested",
|
|
# "opentm":1601489313.3898,
|
|
# "closetm":1601489346.5507,
|
|
# "starttm":0,
|
|
# "expiretm":0,
|
|
# "descr":{
|
|
# "pair":"ETHUSDT",
|
|
# "type":"buy",
|
|
# "ordertype":"limit",
|
|
# "price":"330.00",
|
|
# "price2":"0",
|
|
# "leverage":"none",
|
|
# "order":"buy 0.02100000 ETHUSDT @ limit 330.00",
|
|
# "close":""
|
|
# },
|
|
# "vol":"0.02100000",
|
|
# "vol_exec":"0.00000000",
|
|
# "cost":"0.00000",
|
|
# "fee":"0.00000",
|
|
# "price":"0.00000",
|
|
# "stopprice":"0.00000",
|
|
# "limitprice":"0.00000",
|
|
# "misc":"",
|
|
# "oflags":"fciq"
|
|
# },
|
|
# },
|
|
# "count":16
|
|
# }
|
|
# }
|
|
#
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
result = self.safe_dict(response, 'result', {})
|
|
closed = self.safe_dict(result, 'closed', {})
|
|
orders = []
|
|
orderIds = list(closed.keys())
|
|
for i in range(0, len(orderIds)):
|
|
id = orderIds[i]
|
|
item = closed[id]
|
|
orders.append(self.extend({'id': id}, item))
|
|
return self.parse_orders(orders, market, since, limit)
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
# IFEX transaction states
|
|
statuses: dict = {
|
|
'Initial': 'pending',
|
|
'Pending': 'pending',
|
|
'Success': 'ok',
|
|
'Settled': 'pending',
|
|
'Failure': 'failed',
|
|
'Partial': 'ok',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_network(self, network):
|
|
withdrawMethods = self.safe_value(self.options, 'withdrawMethods', {})
|
|
return self.safe_string(withdrawMethods, network, network)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# fetchDeposits
|
|
#
|
|
# {
|
|
# "method": "Ether(Hex)",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "refid": "Q2CANKL-LBFVEE-U4Y2WQ",
|
|
# "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…",
|
|
# "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290",
|
|
# "amount": "7.9999257900",
|
|
# "fee": "0.0000000000",
|
|
# "time": 1529223212,
|
|
# "status": "Success"
|
|
# }
|
|
#
|
|
# there can be an additional 'status-prop' field present
|
|
# deposit pending review by exchange => 'on-hold'
|
|
# the deposit is initiated by the exchange => 'return'
|
|
#
|
|
# {
|
|
# "type": 'deposit',
|
|
# "method": 'Fidor Bank AG(Wire Transfer)',
|
|
# "aclass": 'currency',
|
|
# "asset": 'ZEUR',
|
|
# "refid": 'xxx-xxx-xxx',
|
|
# "txid": '12341234',
|
|
# "info": 'BANKCODEXXX',
|
|
# "amount": '38769.08',
|
|
# "fee": '0.0000',
|
|
# "time": 1644306552,
|
|
# "status": 'Success',
|
|
# status-prop: 'on-hold'
|
|
# }
|
|
#
|
|
#
|
|
# fetchWithdrawals
|
|
#
|
|
# {
|
|
# "method": "Ether",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "refid": "A2BF34S-O7LBNQ-UE4Y4O",
|
|
# "txid": "0x288b83c6b0904d8400ef44e1c9e2187b5c8f7ea3d838222d53f701a15b5c274d",
|
|
# "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0",
|
|
# "amount": "9.9950000000",
|
|
# "fee": "0.0050000000",
|
|
# "time": 1530481750,
|
|
# "status": "Success"
|
|
# "key":"Huobi wallet",
|
|
# "network":"Tron"
|
|
# status-prop: 'on-hold' # self field might not be present in some cases
|
|
# }
|
|
#
|
|
# withdraw
|
|
#
|
|
# {
|
|
# "refid": "AGBSO6T-UFMTTQ-I7KGS6"
|
|
# }
|
|
#
|
|
id = self.safe_string(transaction, 'refid')
|
|
txid = self.safe_string(transaction, 'txid')
|
|
timestamp = self.safe_timestamp(transaction, 'time')
|
|
currencyId = self.safe_string(transaction, 'asset')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
address = self.safe_string(transaction, 'info')
|
|
amount = self.safe_number(transaction, 'amount')
|
|
status = self.parse_transaction_status(self.safe_string(transaction, 'status'))
|
|
statusProp = self.safe_string(transaction, 'status-prop')
|
|
isOnHoldDeposit = statusProp == 'on-hold'
|
|
isCancellationRequest = statusProp == 'cancel-pending'
|
|
isOnHoldWithdrawal = statusProp == 'onhold'
|
|
if isOnHoldDeposit or isCancellationRequest or isOnHoldWithdrawal:
|
|
status = 'pending'
|
|
type = self.safe_string(transaction, 'type') # injected from the outside
|
|
feeCost = self.safe_number(transaction, 'fee')
|
|
if feeCost is None:
|
|
if type == 'deposit':
|
|
feeCost = 0
|
|
return {
|
|
'info': transaction,
|
|
'id': id,
|
|
'currency': code,
|
|
'amount': amount,
|
|
'network': self.parse_network(self.safe_string(transaction, 'network')),
|
|
'address': address,
|
|
'addressTo': None,
|
|
'addressFrom': None,
|
|
'tag': None,
|
|
'tagTo': None,
|
|
'tagFrom': None,
|
|
'status': status,
|
|
'type': type,
|
|
'updated': None,
|
|
'txid': txid,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'comment': None,
|
|
'internal': None,
|
|
'fee': {
|
|
'currency': code,
|
|
'cost': feeCost,
|
|
},
|
|
}
|
|
|
|
def parse_transactions_by_type(self, type, transactions, code: Str = None, since: Int = None, limit: Int = None):
|
|
result = []
|
|
for i in range(0, len(transactions)):
|
|
transaction = self.parse_transaction(self.extend({
|
|
'type': type,
|
|
}, transactions[i]))
|
|
result.append(transaction)
|
|
return self.filter_by_currency_since_limit(result, code, since, limit)
|
|
|
|
def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all deposits made to an account
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentDeposits
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch deposits for
|
|
:param int [limit]: the maximum number of deposits structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest transaction entry
|
|
:param int [params.end]: timestamp in seconds of the latest transaction entry
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
# https://www.kraken.com/en-us/help/api#deposit-status
|
|
self.load_markets()
|
|
request: dict = {}
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['asset'] = currency['id']
|
|
if since is not None:
|
|
sinceString = self.number_to_string(since)
|
|
request['start'] = Precise.string_div(sinceString, '1000')
|
|
until = self.safe_string_n(params, ['until', 'till'])
|
|
if until is not None:
|
|
params = self.omit(params, ['until', 'till'])
|
|
untilDivided = Precise.string_div(until, '1000')
|
|
request['end'] = Precise.string_add(untilDivided, '1')
|
|
response = self.privatePostDepositStatus(self.extend(request, params))
|
|
#
|
|
# { error: [],
|
|
# "result": [{"method": "Ether(Hex)",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "refid": "Q2CANKL-LBFVEE-U4Y2WQ",
|
|
# "txid": "0x57fd704dab1a73c20e24c8696099b695d596924b401b261513cfdab23…",
|
|
# "info": "0x615f9ba7a9575b0ab4d571b2b36b1b324bd83290",
|
|
# "amount": "7.9999257900",
|
|
# "fee": "0.0000000000",
|
|
# "time": 1529223212,
|
|
# "status": "Success" }]}
|
|
#
|
|
return self.parse_transactions_by_type('deposit', response['result'], code, since, limit)
|
|
|
|
def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
|
|
https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getServerTime
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
# https://www.kraken.com/en-us/features/api#get-server-time
|
|
response = self.publicGetTime(params)
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "unixtime": 1591502873,
|
|
# "rfc1123": "Sun, 7 Jun 20 04:07:53 +0000"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_value(response, 'result', {})
|
|
return self.safe_timestamp(result, 'unixtime')
|
|
|
|
def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all withdrawals made from an account
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/getStatusRecentWithdrawals
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch withdrawals for
|
|
:param int [limit]: the maximum number of withdrawals structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest transaction entry
|
|
:param int [params.end]: timestamp in seconds of the latest transaction entry
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate')
|
|
if paginate:
|
|
params['cursor'] = True
|
|
return self.fetch_paginated_call_cursor('fetchWithdrawals', code, since, limit, params, 'next_cursor', 'cursor')
|
|
request: dict = {}
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['asset'] = currency['id']
|
|
if since is not None:
|
|
sinceString = self.number_to_string(since)
|
|
request['start'] = Precise.string_div(sinceString, '1000')
|
|
until = self.safe_string_n(params, ['until', 'till'])
|
|
if until is not None:
|
|
params = self.omit(params, ['until', 'till'])
|
|
untilDivided = Precise.string_div(until, '1000')
|
|
request['end'] = Precise.string_add(untilDivided, '1')
|
|
response = self.privatePostWithdrawStatus(self.extend(request, params))
|
|
#
|
|
# with no pagination
|
|
# { error: [],
|
|
# "result": [{"method": "Ether",
|
|
# "aclass": "currency",
|
|
# "asset": "XETH",
|
|
# "refid": "A2BF34S-O7LBNQ-UE4Y4O",
|
|
# "txid": "0x298c83c7b0904d8400ef43e1c9e2287b518f7ea3d838822d53f704a1565c274d",
|
|
# "info": "0x7cb275a5e07ba943fee972e165d80daa67cb2dd0",
|
|
# "amount": "9.9950000000",
|
|
# "fee": "0.0050000000",
|
|
# "time": 1530481750,
|
|
# "status": "Success" }]}
|
|
# with pagination
|
|
# {
|
|
# "error":[],
|
|
# "result":{
|
|
# "withdrawals":[
|
|
# {
|
|
# "method":"Tether USD(TRC20)",
|
|
# "aclass":"currency",
|
|
# "asset":"USDT",
|
|
# "refid":"BSNFZU2-MEFN4G-J3NEZV",
|
|
# "txid":"1c7a642fb7387bbc2c6a2c509fd1ae146937f4cf793b4079a4f0715e3a02615a",
|
|
# "info":"TQmdxSuC16EhFg8FZWtYgrfFRosoRF7bCp",
|
|
# "amount":"1996.50000000",
|
|
# "fee":"2.50000000",
|
|
# "time":1669126657,
|
|
# "status":"Success",
|
|
# "key":"poloniex",
|
|
# "network":"Tron"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next_cursor":"HgAAAAAAAABGVFRSd3k1LVF4Y0JQY05Gd0xRY0NxenFndHpybkwBAQH2AwEBAAAAAQAAAAAAAAABAAAAAAAZAAAAAAAAAA=="
|
|
# }
|
|
# }
|
|
#
|
|
rawWithdrawals = None
|
|
result = self.safe_value(response, 'result')
|
|
if not isinstance(result, list):
|
|
rawWithdrawals = self.add_pagination_cursor_to_result(result)
|
|
else:
|
|
rawWithdrawals = result
|
|
return self.parse_transactions_by_type('withdrawal', rawWithdrawals, code, since, limit)
|
|
|
|
def add_pagination_cursor_to_result(self, result):
|
|
cursor = self.safe_string(result, 'next_cursor')
|
|
data = self.safe_value(result, 'withdrawals')
|
|
dataLength = len(data)
|
|
if cursor is not None and dataLength > 0:
|
|
last = data[dataLength - 1]
|
|
last['next_cursor'] = cursor
|
|
data[dataLength - 1] = last
|
|
return data
|
|
|
|
def create_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
create a currency deposit address
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses
|
|
|
|
: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>`
|
|
"""
|
|
request: dict = {
|
|
'new': 'true',
|
|
}
|
|
return self.fetch_deposit_address(code, self.extend(request, params))
|
|
|
|
def fetch_deposit_methods(self, code: str, params={}):
|
|
"""
|
|
fetch deposit methods for a currency associated with self account
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/getDepositMethods
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the kraken api endpoint
|
|
:returns dict: of deposit methods
|
|
"""
|
|
self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'asset': currency['id'],
|
|
}
|
|
response = self.privatePostDepositMethods(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":[
|
|
# {"method":"Ether(Hex)","limit":false,"gen-address":true}
|
|
# ]
|
|
# }
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":[
|
|
# {"method":"Tether USD(ERC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true},
|
|
# {"method":"Tether USD(TRC20)","limit":false,"address-setup-fee":"0.00000000","gen-address":true}
|
|
# ]
|
|
# }
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":[
|
|
# {"method":"Bitcoin","limit":false,"fee":"0.0000000000","gen-address":true}
|
|
# ]
|
|
# }
|
|
#
|
|
return self.safe_value(response, 'result')
|
|
|
|
def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/getDepositAddresses
|
|
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
currency = self.currency(code)
|
|
network = self.safe_string_upper(params, 'network')
|
|
networks = self.safe_value(self.options, 'networks', {})
|
|
network = self.safe_string(networks, network, network) # support ETH > ERC20 aliases
|
|
params = self.omit(params, 'network')
|
|
if (code == 'USDT') and (network == 'TRC20'):
|
|
code = code + '-' + network
|
|
defaultDepositMethods = self.safe_value(self.options, 'depositMethods', {})
|
|
defaultDepositMethod = self.safe_string(defaultDepositMethods, code)
|
|
depositMethod = self.safe_string(params, 'method', defaultDepositMethod)
|
|
# if the user has specified an exchange-specific method in params
|
|
# we pass it, otherwise we take the 'network' unified param
|
|
if depositMethod is None:
|
|
depositMethods = self.fetch_deposit_methods(code)
|
|
if network is not None:
|
|
# find best matching deposit method, or fallback to the first one
|
|
for i in range(0, len(depositMethods)):
|
|
entry = self.safe_string(depositMethods[i], 'method')
|
|
if entry.find(network) >= 0:
|
|
depositMethod = entry
|
|
break
|
|
# if depositMethod was not specified, fallback to the first available deposit method
|
|
if depositMethod is None:
|
|
firstDepositMethod = self.safe_value(depositMethods, 0, {})
|
|
depositMethod = self.safe_string(firstDepositMethod, 'method')
|
|
request: dict = {
|
|
'asset': currency['id'],
|
|
'method': depositMethod,
|
|
}
|
|
response = self.privatePostDepositAddresses(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[],
|
|
# "result":[
|
|
# {"address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3","expiretm":"0"}
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_value(response, 'result', [])
|
|
firstResult = self.safe_value(result, 0, {})
|
|
if firstResult is None:
|
|
raise InvalidAddress(self.id + ' privatePostDepositAddresses() returned no addresses for ' + code)
|
|
return self.parse_deposit_address(firstResult, currency)
|
|
|
|
def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress:
|
|
#
|
|
# {
|
|
# "address":"0x77b5051f97efa9cc52c9ad5b023a53fc15c200d3",
|
|
# "expiretm":"0"
|
|
# }
|
|
#
|
|
address = self.safe_string(depositAddress, 'address')
|
|
tag = self.safe_string(depositAddress, 'tag')
|
|
currency = self.safe_currency(None, currency)
|
|
code = currency['code']
|
|
self.check_address(address)
|
|
return {
|
|
'info': depositAddress,
|
|
'currency': code,
|
|
'network': None,
|
|
'address': address,
|
|
'tag': tag,
|
|
}
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://docs.kraken.com/rest/#tag/Funding/operation/withdrawFunds
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to, not required can be '' or None/none/None
|
|
:param str tag:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
tag, params = self.handle_withdraw_tag_and_params(tag, params)
|
|
if 'key' in params:
|
|
self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'asset': currency['id'],
|
|
'amount': amount,
|
|
# 'address': address,
|
|
}
|
|
if address is not None and address != '':
|
|
request['address'] = address
|
|
self.check_address(address)
|
|
response = self.privatePostWithdraw(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# "refid": "AGBSO6T-UFMTTQ-I7KGS6"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_transaction(result, currency)
|
|
raise ExchangeError(self.id + " withdraw() requires a 'key' parameter(withdrawal key name, up on your account)")
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://docs.kraken.com/rest/#tag/Account-Data/operation/getOpenPositions
|
|
|
|
:param str[] [symbols]: not used by kraken fetchPositions()
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
# 'txid': 'comma delimited list of transaction ids to restrict output to',
|
|
'docalcs': 'true', # whether or not to include profit/loss calculations
|
|
'consolidation': 'market', # what to consolidate the positions data around, market will consolidate positions based on market pair
|
|
}
|
|
response = self.privatePostOpenPositions(self.extend(request, params))
|
|
#
|
|
# no consolidation
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": {
|
|
# 'TGUFMY-FLESJ-VYIX3J': {
|
|
# "ordertxid": "O3LRNU-ZKDG5-XNCDFR",
|
|
# "posstatus": "open",
|
|
# "pair": "ETHUSDT",
|
|
# "time": 1611557231.4584,
|
|
# "type": "buy",
|
|
# "ordertype": "market",
|
|
# "cost": "28.49800",
|
|
# "fee": "0.07979",
|
|
# "vol": "0.02000000",
|
|
# "vol_closed": "0.00000000",
|
|
# "margin": "14.24900",
|
|
# "terms": "0.0200% per 4 hours",
|
|
# "rollovertm": "1611571631",
|
|
# "misc": "",
|
|
# "oflags": ""
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# consolidation by market
|
|
#
|
|
# {
|
|
# "error": [],
|
|
# "result": [
|
|
# {
|
|
# "pair": "ETHUSDT",
|
|
# "positions": "1",
|
|
# "type": "buy",
|
|
# "leverage": "2.00000",
|
|
# "cost": "28.49800",
|
|
# "fee": "0.07979",
|
|
# "vol": "0.02000000",
|
|
# "vol_closed": "0.00000000",
|
|
# "margin": "14.24900"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
symbols = self.market_symbols(symbols)
|
|
result = self.safe_list(response, 'result')
|
|
results = self.parse_positions(result, symbols)
|
|
return self.filter_by_array_positions(results, 'symbol', symbols, False)
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# {
|
|
# "pair": "ETHUSDT",
|
|
# "positions": "1",
|
|
# "type": "buy",
|
|
# "leverage": "2.00000",
|
|
# "cost": "28.49800",
|
|
# "fee": "0.07979",
|
|
# "vol": "0.02000000",
|
|
# "vol_closed": "0.00000000",
|
|
# "margin": "14.24900"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 'pair')
|
|
rawSide = self.safe_string(position, 'type')
|
|
side = 'long' if (rawSide == 'buy') else 'short'
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'notional': None,
|
|
'marginMode': None,
|
|
'liquidationPrice': None,
|
|
'entryPrice': None,
|
|
'unrealizedPnl': self.safe_number(position, 'net'),
|
|
'realizedPnl': None,
|
|
'percentage': None,
|
|
'contracts': self.safe_number(position, 'vol'),
|
|
'contractSize': None,
|
|
'markPrice': None,
|
|
'lastPrice': None,
|
|
'side': side,
|
|
'hedged': None,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'lastUpdateTimestamp': None,
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'collateral': None,
|
|
'initialMargin': self.safe_number(position, 'margin'),
|
|
'initialMarginPercentage': None,
|
|
'leverage': self.safe_number(position, 'leverage'),
|
|
'marginRatio': None,
|
|
'stopLossPrice': None,
|
|
'takeProfitPrice': None,
|
|
})
|
|
|
|
def parse_account_type(self, account):
|
|
accountByType: dict = {
|
|
'spot': 'Spot Wallet',
|
|
'swap': 'Futures Wallet',
|
|
'future': 'Futures Wallet',
|
|
}
|
|
return self.safe_string(accountByType, account, account)
|
|
|
|
def transfer_out(self, code: str, amount, params={}):
|
|
"""
|
|
transfer from spot wallet to futures wallet
|
|
|
|
https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer
|
|
|
|
:param str code: Unified currency code
|
|
:param float amount: Size of the transfer
|
|
:param dict [params]: Exchange specific parameters
|
|
:returns: a `transfer structure <https://docs.ccxt.com/#/?id=transfer-structure>`
|
|
"""
|
|
return self.transfer(code, amount, 'spot', 'swap', params)
|
|
|
|
def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
|
|
https://docs.kraken.com/rest/#tag/User-Funding/operation/walletTransfer
|
|
|
|
transfers currencies between sub-accounts(only spot->swap direction is supported)
|
|
:param str code: Unified currency code
|
|
:param float amount: Size of the transfer
|
|
:param str fromAccount: 'spot' or 'Spot Wallet'
|
|
:param str toAccount: 'swap' or 'Futures Wallet'
|
|
:param dict [params]: Exchange specific parameters
|
|
:returns: a `transfer structure <https://docs.ccxt.com/#/?id=transfer-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = self.currency(code)
|
|
fromAccount = self.parse_account_type(fromAccount)
|
|
toAccount = self.parse_account_type(toAccount)
|
|
request: dict = {
|
|
'amount': self.currency_to_precision(code, amount),
|
|
'from': fromAccount,
|
|
'to': toAccount,
|
|
'asset': currency['id'],
|
|
}
|
|
if fromAccount != 'Spot Wallet':
|
|
raise BadRequest(self.id + ' transfer cannot transfer from ' + fromAccount + ' to ' + toAccount + '. Use krakenfutures instead to transfer from the futures account.')
|
|
response = self.privatePostWalletTransfer(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error":[
|
|
# ],
|
|
# "result":{
|
|
# "refid":"BOIUSIF-M7DLMN-UXZ3P5"
|
|
# }
|
|
# }
|
|
#
|
|
transfer = self.parse_transfer(response, currency)
|
|
return self.extend(transfer, {
|
|
'amount': amount,
|
|
'fromAccount': fromAccount,
|
|
'toAccount': toAccount,
|
|
})
|
|
|
|
def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
|
|
#
|
|
# transfer
|
|
#
|
|
# {
|
|
# "error":[
|
|
# ],
|
|
# "result":{
|
|
# "refid":"BOIUSIF-M7DLMN-UXZ3P5"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_value(transfer, 'result', {})
|
|
refid = self.safe_string(result, 'refid')
|
|
return {
|
|
'info': transfer,
|
|
'id': refid,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'currency': self.safe_string(currency, 'code'),
|
|
'amount': None,
|
|
'fromAccount': None,
|
|
'toAccount': None,
|
|
'status': 'sucess',
|
|
}
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
url = '/' + self.version + '/' + api + '/' + path
|
|
if api == 'public':
|
|
if params:
|
|
# urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872
|
|
url += '?' + self.urlencode_nested(params)
|
|
elif api == 'private':
|
|
price = self.safe_string(params, 'price')
|
|
isTriggerPercent = False
|
|
if price is not None:
|
|
isTriggerPercent = True if (price.endswith('%')) else False
|
|
isCancelOrderBatch = (path == 'CancelOrderBatch')
|
|
isBatchOrder = (path == 'AddOrderBatch')
|
|
self.check_required_credentials()
|
|
nonce = str(self.nonce())
|
|
# urlencodeNested is used to address https://github.com/ccxt/ccxt/issues/12872
|
|
if isCancelOrderBatch or isTriggerPercent or isBatchOrder:
|
|
body = self.json(self.extend({'nonce': nonce}, params))
|
|
else:
|
|
body = self.urlencode_nested(self.extend({'nonce': nonce}, params))
|
|
auth = self.encode(nonce + body)
|
|
hash = self.hash(auth, 'sha256', 'binary')
|
|
binary = self.encode(url)
|
|
binhash = self.binary_concat(binary, hash)
|
|
secret = self.base64_to_binary(self.secret)
|
|
signature = self.hmac(binhash, secret, hashlib.sha512, 'base64')
|
|
headers = {
|
|
'API-Key': self.apiKey,
|
|
'API-Sign': signature,
|
|
}
|
|
if isCancelOrderBatch or isTriggerPercent or isBatchOrder:
|
|
headers['Content-Type'] = 'application/json'
|
|
else:
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
else:
|
|
url = '/' + path
|
|
url = self.urls['api'][api] + url
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def nonce(self):
|
|
return self.milliseconds() - self.options['timeDifference']
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if code == 520:
|
|
raise ExchangeNotAvailable(self.id + ' ' + str(code) + ' ' + reason)
|
|
if response is None:
|
|
return None
|
|
if body[0] == '{':
|
|
if not isinstance(response, str):
|
|
message = self.id + ' ' + body
|
|
if 'error' in response:
|
|
numErrors = len(response['error'])
|
|
if numErrors:
|
|
for i in range(0, len(response['error'])):
|
|
error = response['error'][i]
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], error, message)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], error, message)
|
|
raise ExchangeError(message)
|
|
# handleCreateOrdersErrors:
|
|
if 'result' in response:
|
|
result = self.safe_dict(response, 'result', {})
|
|
if 'orders' in result:
|
|
orders = self.safe_list(result, 'orders', [])
|
|
for i in range(0, len(orders)):
|
|
order = orders[i]
|
|
error = self.safe_string(order, 'error')
|
|
if error is not None:
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], error, message)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], error, message)
|
|
raise ExchangeError(message)
|
|
return None
|