From 05122af764e564a5024f3aefd6479efaf32c0ba3 Mon Sep 17 00:00:00 2001 From: "zhangkun9038@dingtalk.com" Date: Fri, 25 Apr 2025 15:23:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E4=BA=86=E4=B8=80=E4=B8=AAcc?= =?UTF-8?q?xt=E7=9A=84bug,=20okx=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=9C=89baseCcy=E6=88=96=E8=80=85quoteCcy?= =?UTF-8?q?=E4=B8=BAnull,=20=E6=8C=82=E8=BD=BD=E4=BA=86=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=96=87=E4=BB=B6=E6=9B=BF=E6=8D=A2=E4=BA=86?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=86=85=E7=9A=84=E7=89=88=E6=9C=AC,=20?= =?UTF-8?q?=E7=AE=97=E6=98=AF=E4=B8=B4=E6=97=B6=E8=A7=A3=E5=86=B3=E4=BA=86?= =?UTF-8?q?,=E4=BB=A5=E5=90=8E=E8=A6=81=E8=80=83=E8=99=91=E5=A6=82?= =?UTF-8?q?=E4=BD=95=E6=8C=81=E7=BB=AD=E9=9B=86=E6=88=90=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccxt/async_support/okx.py | 8325 ++++++++++++++++++ config_examples/config_freqai.okx.json | 9 +- docker-compose.yml | 1 + freqtrade/templates/FreqaiExampleStrategy.py | 114 +- 4 files changed, 8380 insertions(+), 69 deletions(-) create mode 100644 ccxt/async_support/okx.py diff --git a/ccxt/async_support/okx.py b/ccxt/async_support/okx.py new file mode 100644 index 0000000..1a86bb8 --- /dev/null +++ b/ccxt/async_support/okx.py @@ -0,0 +1,8325 @@ +# -*- coding: utf-8 -*- + +# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: +# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code + +from ccxt.async_support.base.exchange import Exchange +from ccxt.abstract.okx import ImplicitAPI +import asyncio +import hashlib +from ccxt.base.types import Account, Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, CrossBorrowRates, Currencies, Currency, DepositAddress, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LongShortRatio, MarginModification, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, Trade, TradingFeeInterface, Transaction, MarketInterface, 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 AccountNotEnabled +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 OperationRejected +from ccxt.base.errors import ManualInteractionNeeded +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 ContractUnavailable +from ccxt.base.errors import NotSupported +from ccxt.base.errors import NetworkError +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 RequestTimeout +from ccxt.base.errors import CancelPending +from ccxt.base.decimal_to_precision import TICK_SIZE +from ccxt.base.precise import Precise + + +class okx(Exchange, ImplicitAPI): + + def describe(self) -> Any: + return self.deep_extend(super(okx, self).describe(), { + 'id': 'okx', + 'name': 'OKX', + 'countries': ['CN', 'US'], + 'version': 'v5', + 'rateLimit': 100 * 1.03, # 3% tolerance because of #20229 + 'pro': True, + 'certified': True, + 'has': { + 'CORS': None, + 'spot': True, + 'margin': True, + 'swap': True, + 'future': True, + 'option': True, + 'addMargin': True, + 'cancelAllOrders': False, + 'cancelAllOrdersAfter': True, + 'cancelOrder': True, + 'cancelOrders': True, + 'cancelOrdersForSymbols': True, + 'closeAllPositions': False, + 'closePosition': True, + 'createConvertTrade': True, + 'createDepositAddress': False, + 'createMarketBuyOrderWithCost': True, + 'createMarketSellOrderWithCost': True, + 'createOrder': True, + 'createOrders': True, + 'createOrderWithTakeProfitAndStopLoss': True, + 'createPostOnlyOrder': True, + 'createReduceOnlyOrder': True, + 'createStopLimitOrder': True, + 'createStopLossOrder': True, + 'createStopMarketOrder': True, + 'createStopOrder': True, + 'createTakeProfitOrder': True, + 'createTrailingPercentOrder': True, + 'createTriggerOrder': True, + 'editOrder': True, + 'fetchAccounts': True, + 'fetchBalance': True, + 'fetchBidsAsks': None, + 'fetchBorrowInterest': True, + 'fetchBorrowRateHistories': True, + 'fetchBorrowRateHistory': True, + 'fetchCanceledOrders': True, + 'fetchClosedOrder': None, + 'fetchClosedOrders': True, + 'fetchConvertCurrencies': True, + 'fetchConvertQuote': True, + 'fetchConvertTrade': True, + 'fetchConvertTradeHistory': True, + 'fetchCrossBorrowRate': True, + 'fetchCrossBorrowRates': True, + 'fetchCurrencies': True, + 'fetchDeposit': True, + 'fetchDepositAddress': True, + 'fetchDepositAddresses': False, + 'fetchDepositAddressesByNetwork': True, + 'fetchDeposits': True, + 'fetchDepositsWithdrawals': False, + 'fetchDepositWithdrawFee': 'emulated', + 'fetchDepositWithdrawFees': True, + 'fetchFundingHistory': True, + 'fetchFundingInterval': True, + 'fetchFundingIntervals': False, + 'fetchFundingRate': True, + 'fetchFundingRateHistory': True, + 'fetchFundingRates': False, + 'fetchGreeks': True, + 'fetchIndexOHLCV': True, + 'fetchIsolatedBorrowRate': False, + 'fetchIsolatedBorrowRates': False, + 'fetchL3OrderBook': False, + 'fetchLedger': True, + 'fetchLedgerEntry': None, + 'fetchLeverage': True, + 'fetchLeverageTiers': False, + 'fetchLongShortRatio': False, + 'fetchLongShortRatioHistory': True, + 'fetchMarginAdjustmentHistory': True, + 'fetchMarketLeverageTiers': True, + 'fetchMarkets': True, + 'fetchMarkOHLCV': True, + 'fetchMarkPrice': True, + 'fetchMarkPrices': True, + 'fetchMySettlementHistory': False, + 'fetchMyTrades': True, + 'fetchOHLCV': True, + 'fetchOpenInterest': True, + 'fetchOpenInterestHistory': True, + 'fetchOpenOrder': None, + 'fetchOpenOrders': True, + 'fetchOption': True, + 'fetchOptionChain': True, + 'fetchOrder': True, + 'fetchOrderBook': True, + 'fetchOrderBooks': False, + 'fetchOrders': False, + 'fetchOrderTrades': True, + 'fetchPosition': True, + 'fetchPositionHistory': 'emulated', + 'fetchPositions': True, + 'fetchPositionsForSymbol': True, + 'fetchPositionsHistory': True, + 'fetchPositionsRisk': False, + 'fetchPremiumIndexOHLCV': False, + 'fetchSettlementHistory': True, + 'fetchStatus': True, + 'fetchTicker': True, + 'fetchTickers': True, + 'fetchTime': True, + 'fetchTrades': True, + 'fetchTradingFee': True, + 'fetchTradingFees': False, + 'fetchTradingLimits': False, + 'fetchTransactionFee': False, + 'fetchTransactionFees': False, + 'fetchTransactions': False, + 'fetchTransfer': True, + 'fetchTransfers': True, + 'fetchUnderlyingAssets': True, + 'fetchVolatilityHistory': False, + 'fetchWithdrawal': True, + 'fetchWithdrawals': True, + 'fetchWithdrawalWhitelist': False, + 'reduceMargin': True, + 'repayCrossMargin': True, + 'sandbox': True, + 'setLeverage': True, + 'setMargin': False, + 'setMarginMode': True, + 'setPositionMode': True, + 'signIn': False, + 'transfer': True, + 'withdraw': True, + }, + 'timeframes': { + '1m': '1m', + '3m': '3m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1H', + '2h': '2H', + '4h': '4H', + '6h': '6H', + '12h': '12H', + '1d': '1D', + '1w': '1W', + '1M': '1M', + '3M': '3M', + }, + 'hostname': 'www.okx.com', # or aws.okx.com + 'urls': { + 'logo': 'https://user-images.githubusercontent.com/1294454/152485636-38b19e4a-bece-4dec-979a-5982859ffc04.jpg', + 'api': { + 'rest': 'https://{hostname}', + }, + 'www': 'https://www.okx.com', + 'doc': 'https://www.okx.com/docs-v5/en/', + 'fees': 'https://www.okx.com/pages/products/fees.html', + 'referral': { + # old reflink 0% discount https://www.okx.com/join/1888677 + # new reflink 20% discount https://www.okx.com/join/CCXT2023 + 'url': 'https://www.okx.com/join/CCXT2023', + 'discount': 0.2, + }, + 'test': { + 'rest': 'https://{hostname}', + }, + }, + 'api': { + 'public': { + 'get': { + 'market/books-full': 2, + 'market/tickers': 1, + 'market/ticker': 1, + 'market/index-tickers': 1, + 'market/books': 1 / 2, + 'market/books-lite': 5 / 3, + 'market/candles': 1 / 2, + 'market/history-candles': 1, + 'market/index-candles': 1, + 'market/history-index-candles': 2, + 'market/mark-price-candles': 1, + 'market/history-mark-price-candles': 2, + 'market/trades': 1 / 5, + 'market/history-trades': 2, + 'market/option/instrument-family-trades': 1, + 'market/platform-24-volume': 10, + 'market/open-oracle': 50, + 'market/exchange-rate': 20, + 'market/index-components': 1, + 'public/economic-calendar': 50, + 'market/block-tickers': 1, + 'market/block-ticker': 1, + 'public/block-trades': 1, + 'public/instruments': 1, + 'public/delivery-exercise-history': 1 / 2, + 'public/open-interest': 1, + 'public/funding-rate': 1, + 'public/funding-rate-history': 1, + 'public/price-limit': 1, + 'public/opt-summary': 1, + 'public/estimated-price': 2, + 'public/discount-rate-interest-free-quota': 10, + 'public/time': 2, + 'public/mark-price': 2, + 'public/position-tiers': 2, + 'public/interest-rate-loan-quota': 10, + 'public/vip-interest-rate-loan-quota': 10, + 'public/underlying': 1, + 'public/insurance-fund': 2, + 'public/convert-contract-coin': 2, + 'public/option-trades': 1, + 'public/instrument-tick-bands': 4, + 'rubik/stat/trading-data/support-coin': 4, + 'rubik/stat/taker-volume': 4, + 'rubik/stat/margin/loan-ratio': 4, + # long/short + 'rubik/stat/contracts/long-short-account-ratio': 4, + 'rubik/stat/contracts/long-short-account-ratio-contract': 4, + 'rubik/stat/contracts/open-interest-volume': 4, + 'rubik/stat/option/open-interest-volume': 4, + # put/call + 'rubik/stat/option/open-interest-volume-ratio': 4, + 'rubik/stat/option/open-interest-volume-expiry': 4, + 'rubik/stat/option/open-interest-volume-strike': 4, + 'rubik/stat/option/taker-block-volume': 4, + 'system/status': 50, + # public api + 'sprd/spreads': 1, + 'sprd/books': 1 / 2, + 'sprd/ticker': 1, + 'sprd/public-trades': 1 / 5, + 'market/sprd-ticker': 2, + 'market/sprd-candles': 2, + 'market/sprd-history-candles': 2, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/public/rsi-back-testing': 1, + 'asset/exchange-list': 5 / 3, + 'finance/staking-defi/eth/apy-history': 5 / 3, + 'finance/staking-defi/sol/apy-history': 5 / 3, + 'finance/savings/lending-rate-summary': 5 / 3, + 'finance/savings/lending-rate-history': 5 / 3, + 'finance/fixed-loan/lending-offers': 10 / 3, + 'finance/fixed-loan/lending-apy-history': 10 / 3, + 'finance/fixed-loan/pending-lending-volume': 10 / 3, + # public broker + 'finance/sfp/dcd/products': 2 / 3, + # copytrading + 'copytrading/public-lead-traders': 4, + 'copytrading/public-weekly-pnl': 4, + 'copytrading/public-stats': 4, + 'copytrading/public-preference-currency': 4, + 'copytrading/public-current-subpositions': 4, + 'copytrading/public-subpositions-history': 4, + 'support/announcements-types': 20, + }, + }, + 'private': { + 'get': { + # rfq + 'rfq/counterparties': 4, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-config': 4, + 'rfq/rfqs': 10, + 'rfq/quotes': 10, + 'rfq/trades': 4, + 'rfq/public-trades': 4, + # sprd + 'sprd/order': 1 / 3, + 'sprd/orders-pending': 1 / 3, + 'sprd/orders-history': 1 / 2, + 'sprd/orders-history-archive': 1 / 2, + 'sprd/trades': 1 / 3, + # trade + 'trade/order': 1 / 3, + 'trade/orders-pending': 1 / 3, + 'trade/orders-history': 1 / 2, + 'trade/orders-history-archive': 1, + 'trade/fills': 1 / 3, + 'trade/fills-history': 2.2, + 'trade/fills-archive': 2, + 'trade/order-algo': 1, + 'trade/orders-algo-pending': 1, + 'trade/orders-algo-history': 1, + 'trade/easy-convert-currency-list': 20, + 'trade/easy-convert-history': 20, + 'trade/one-click-repay-currency-list': 20, + 'trade/one-click-repay-history': 20, + 'trade/account-rate-limit': 1, + # asset + 'asset/currencies': 5 / 3, + 'asset/balances': 5 / 3, + 'asset/non-tradable-assets': 5 / 3, + 'asset/asset-valuation': 10, + 'asset/transfer-state': 10, + 'asset/bills': 5 / 3, + 'asset/deposit-lightning': 5, + 'asset/deposit-address': 5 / 3, + 'asset/deposit-history': 5 / 3, + 'asset/withdrawal-history': 5 / 3, + 'asset/deposit-withdraw-status': 20, + 'asset/convert/currencies': 5 / 3, + 'asset/convert/currency-pair': 5 / 3, + 'asset/convert/history': 5 / 3, + 'asset/monthly-statement': 2, + # account + 'account/instruments': 1, + 'account/balance': 2, + 'account/positions': 2, + 'account/positions-history': 100, + 'account/account-position-risk': 2, + 'account/bills': 5 / 3, + 'account/bills-archive': 5 / 3, + 'account/bills-history-archive': 2, + 'account/config': 4, + 'account/max-size': 1, + 'account/max-avail-size': 1, + 'account/leverage-info': 1, + 'account/adjust-leverage-info': 4, + 'account/max-loan': 1, + 'account/trade-fee': 4, + 'account/interest-accrued': 4, + 'account/interest-rate': 4, + 'account/max-withdrawal': 1, + 'account/risk-state': 2, + 'account/quick-margin-borrow-repay-history': 4, + 'account/borrow-repay-history': 4, + 'account/vip-interest-accrued': 4, + 'account/vip-interest-deducted': 4, + 'account/vip-loan-order-list': 4, + 'account/vip-loan-order-detail': 4, + 'account/interest-limits': 4, + 'account/greeks': 2, + 'account/position-tiers': 2, + 'account/mmp-config': 4, + 'account/fixed-loan/borrowing-limit': 4, + 'account/fixed-loan/borrowing-quote': 5, + 'account/fixed-loan/borrowing-orders-list': 5, + 'account/spot-manual-borrow-repay': 10, + 'account/set-auto-repay': 4, + 'account/spot-borrow-repay-history': 4, + # subaccount + 'users/subaccount/list': 10, + 'account/subaccount/balances': 10 / 3, + 'asset/subaccount/balances': 10 / 3, + 'account/subaccount/max-withdrawal': 1, + 'asset/subaccount/bills': 5 / 3, + 'asset/subaccount/managed-subaccount-bills': 5 / 3, + 'users/entrust-subaccount-list': 10, + 'account/subaccount/interest-limits': 4, + # grid trading + 'tradingBot/grid/orders-algo-pending': 1, + 'tradingBot/grid/orders-algo-history': 1, + 'tradingBot/grid/orders-algo-details': 1, + 'tradingBot/grid/sub-orders': 1, + 'tradingBot/grid/positions': 1, + 'tradingBot/grid/ai-param': 1, + 'tradingBot/signal/signals': 1, + 'tradingBot/signal/orders-algo-details': 1, + 'tradingBot/signal/orders-algo-history': 1, + 'tradingBot/signal/positions': 1, + 'tradingBot/signal/positions-history': 1, + 'tradingBot/signal/sub-orders': 1, + 'tradingBot/signal/event-history': 1, + 'tradingBot/recurring/orders-algo-pending': 1, + 'tradingBot/recurring/orders-algo-history': 1, + 'tradingBot/recurring/orders-algo-details': 1, + 'tradingBot/recurring/sub-orders': 1, + # earn + 'finance/savings/balance': 5 / 3, + 'finance/savings/lending-history': 5 / 3, + 'finance/staking-defi/offers': 10 / 3, + 'finance/staking-defi/orders-active': 10 / 3, + 'finance/staking-defi/orders-history': 10 / 3, + # eth staking + 'finance/staking-defi/eth/balance': 5 / 3, + 'finance/staking-defi/eth/purchase-redeem-history': 5 / 3, + 'finance/staking-defi/eth/product-info': 3, + 'finance/staking-defi/sol/balance': 5 / 3, + 'finance/staking-defi/sol/purchase-redeem-history': 5 / 3, + # copytrading + 'copytrading/current-subpositions': 1, + 'copytrading/subpositions-history': 1, + 'copytrading/instruments': 4, + 'copytrading/profit-sharing-details': 4, + 'copytrading/total-profit-sharing': 4, + 'copytrading/unrealized-profit-sharing-details': 4, + 'copytrading/copy-settings': 4, + 'copytrading/batch-leverage-info': 4, + 'copytrading/current-lead-traders': 4, + 'copytrading/lead-traders-history': 4, + # broker + 'broker/nd/info': 10, + 'broker/nd/subaccount-info': 10, + 'broker/nd/subaccount/apikey': 10, + 'asset/broker/nd/subaccount-deposit-address': 5 / 3, + 'asset/broker/nd/subaccount-deposit-history': 4, + 'asset/broker/nd/subaccount-withdrawal-history': 4, + 'broker/nd/rebate-daily': 100, + 'broker/nd/rebate-per-orders': 300, + 'finance/sfp/dcd/order': 2, + 'finance/sfp/dcd/orders': 2, + 'broker/fd/rebate-per-orders': 300, + 'broker/fd/if-rebate': 5, + # affiliate + 'affiliate/invitee/detail': 1, + 'users/partner/if-rebate': 1, + 'support/announcements': 4, + }, + 'post': { + # rfq + 'rfq/create-rfq': 4, + 'rfq/cancel-rfq': 4, + 'rfq/cancel-batch-rfqs': 10, + 'rfq/cancel-all-rfqs': 10, + 'rfq/execute-quote': 15, + 'rfq/maker-instrument-settings': 4, + 'rfq/mmp-reset': 4, + 'rfq/mmp-config': 100, + 'rfq/create-quote': 0.4, + 'rfq/cancel-quote': 0.4, + 'rfq/cancel-batch-quotes': 10, + 'rfq/cancel-all-quotes': 10, + # sprd + 'sprd/order': 1, + 'sprd/cancel-order': 1, + 'sprd/mass-cancel': 1, + 'sprd/amend-order': 1, + 'sprd/cancel-all-after': 10, + # trade + 'trade/order': 1 / 3, + 'trade/batch-orders': 1 / 15, + 'trade/cancel-order': 1 / 3, + 'trade/cancel-batch-orders': 1 / 15, + 'trade/amend-order': 1 / 3, + 'trade/amend-batch-orders': 1 / 150, + 'trade/close-position': 1, + 'trade/fills-archive': 172800, # 5 req per day = 5/24/60/60 => 10/5*24*60*60=172800 + 'trade/order-algo': 1, + 'trade/cancel-algos': 1, + 'trade/amend-algos': 1, + 'trade/cancel-advance-algos': 1, + 'trade/easy-convert': 20, + 'trade/one-click-repay': 20, + 'trade/mass-cancel': 4, + 'trade/cancel-all-after': 10, + # asset + 'asset/transfer': 10, + 'asset/withdrawal': 5 / 3, + 'asset/withdrawal-lightning': 5, + 'asset/cancel-withdrawal': 5 / 3, + 'asset/convert-dust-assets': 10, + 'asset/convert/estimate-quote': 1, + 'asset/convert/trade': 1, + 'asset/monthly-statement': 1, + # account + 'account/set-position-mode': 4, + 'account/set-leverage': 1, + 'account/position/margin-balance': 1, + 'account/set-greeks': 4, + 'account/set-isolated-mode': 4, + 'account/quick-margin-borrow-repay': 4, + 'account/borrow-repay': 5 / 3, + 'account/simulated_margin': 10, + 'account/position-builder': 10, + 'account/set-riskOffset-type': 2, + 'account/activate-option': 4, + 'account/set-auto-loan': 4, + 'account/set-account-level': 4, + 'account/mmp-reset': 4, + 'account/mmp-config': 100, + 'account/fixed-loan/borrowing-order': 5, + 'account/fixed-loan/amend-borrowing-order': 5, + 'account/fixed-loan/manual-reborrow': 5, + 'account/fixed-loan/repay-borrowing-order': 5, + 'account/bills-history-archive': 72000, # 12 req/day + # subaccount + 'users/subaccount/modify-apikey': 10, + 'asset/subaccount/transfer': 10, + 'users/subaccount/set-transfer-out': 10, + 'account/subaccount/set-loan-allocation': 4, + # grid trading + 'tradingBot/grid/order-algo': 1, + 'tradingBot/grid/amend-order-algo': 1, + 'tradingBot/grid/stop-order-algo': 1, + 'tradingBot/grid/close-position': 1, + 'tradingBot/grid/cancel-close-order': 1, + 'tradingBot/grid/order-instant-trigger': 1, + 'tradingBot/grid/withdraw-income': 1, + 'tradingBot/grid/compute-margin-balance': 1, + 'tradingBot/grid/margin-balance': 1, + 'tradingBot/grid/min-investment': 1, + 'tradingBot/grid/adjust-investment': 1, + 'tradingBot/signal/create-signal': 1, + 'tradingBot/signal/order-algo': 1, + 'tradingBot/signal/stop-order-algo': 1, + 'tradingBot/signal/margin-balance': 1, + 'tradingBot/signal/amendTPSL': 1, + 'tradingBot/signal/set-instruments': 1, + 'tradingBot/signal/close-position': 1, + 'tradingBot/signal/sub-order': 1, + 'tradingBot/signal/cancel-sub-order': 1, + 'tradingBot/recurring/order-algo': 1, + 'tradingBot/recurring/amend-order-algo': 1, + 'tradingBot/recurring/stop-order-algo': 1, + # earn + 'finance/savings/purchase-redempt': 5 / 3, + 'finance/savings/set-lending-rate': 5 / 3, + 'finance/staking-defi/purchase': 3, + 'finance/staking-defi/redeem': 3, + 'finance/staking-defi/cancel': 3, + # eth staking + 'finance/staking-defi/eth/purchase': 5, + 'finance/staking-defi/eth/redeem': 5, + 'finance/staking-defi/sol/purchase': 5, + 'finance/staking-defi/sol/redeem': 5, + # copytrading + 'copytrading/algo-order': 1, + 'copytrading/close-subposition': 1, + 'copytrading/set-instruments': 4, + 'copytrading/first-copy-settings': 4, + 'copytrading/amend-copy-settings': 4, + 'copytrading/stop-copy-trading': 4, + 'copytrading/batch-set-leverage': 4, + # broker + 'broker/nd/create-subaccount': 0.25, + 'broker/nd/delete-subaccount': 1, + 'broker/nd/subaccount/apikey': 0.25, + 'broker/nd/subaccount/modify-apikey': 1, + 'broker/nd/subaccount/delete-apikey': 1, + 'broker/nd/set-subaccount-level': 4, + 'broker/nd/set-subaccount-fee-rate': 4, + 'broker/nd/set-subaccount-assets': 0.25, + 'asset/broker/nd/subaccount-deposit-address': 1, + 'asset/broker/nd/modify-subaccount-deposit-address': 5 / 3, + 'broker/nd/rebate-per-orders': 36000, + 'finance/sfp/dcd/quote': 10, + 'finance/sfp/dcd/order': 10, + 'broker/nd/report-subaccount-ip': 0.25, + 'broker/fd/rebate-per-orders': 36000, + }, + }, + }, + 'fees': { + 'trading': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'spot': { + 'taker': self.parse_number('0.0015'), + 'maker': self.parse_number('0.0010'), + }, + 'future': { + 'taker': self.parse_number('0.0005'), + 'maker': self.parse_number('0.0002'), + }, + 'swap': { + 'taker': self.parse_number('0.00050'), + 'maker': self.parse_number('0.00020'), + }, + }, + 'requiredCredentials': { + 'apiKey': True, + 'secret': True, + 'password': True, + }, + 'exceptions': { + 'exact': { + # Public error codes from 50000-53999 + # General Class + '1': ExchangeError, # Operation failed + '2': ExchangeError, # Bulk operation partially succeeded + '4088': ManualInteractionNeeded, # {"code":"4088","data":[],"msg":"You can’t trade or deposit until you’ve verified your identity again. Head to Identity Verification to complete it."} + '50000': BadRequest, # Body can not be empty + '50001': OnMaintenance, # Matching engine upgrading. Please try again later + '50002': BadRequest, # Json data format error + '50004': RequestTimeout, # Endpoint request timeout(does not indicate success or failure of order, please check order status) + '50005': ExchangeNotAvailable, # API is offline or unavailable + '50006': BadRequest, # Invalid Content_Type, please use "application/json" format + '50007': AccountSuspended, # Account blocked + '50008': AuthenticationError, # User does not exist + '50009': AccountSuspended, # Account is suspended due to ongoing liquidation + '50010': ExchangeError, # User ID can not be empty + '50011': RateLimitExceeded, # Request too frequent + '50012': ExchangeError, # Account status invalid + '50013': ExchangeNotAvailable, # System is busy, please try again later + '50014': BadRequest, # Parameter {0} can not be empty + '50015': ExchangeError, # Either parameter {0} or {1} is required + '50016': ExchangeError, # Parameter {0} does not match parameter {1} + '50017': ExchangeError, # The position is frozen due to ADL. Operation restricted + '50018': ExchangeError, # Currency {0} is frozen due to ADL. Operation restricted + '50019': ExchangeError, # The account is frozen due to ADL. Operation restricted + '50020': ExchangeError, # The position is frozen due to liquidation. Operation restricted + '50021': ExchangeError, # Currency {0} is frozen due to liquidation. Operation restricted + '50022': ExchangeError, # The account is frozen due to liquidation. Operation restricted + '50023': ExchangeError, # Funding fee frozen. Operation restricted + '50024': BadRequest, # Parameter {0} and {1} can not exist at the same time + '50025': ExchangeError, # Parameter {0} count exceeds the limit {1} + '50026': ExchangeNotAvailable, # System error, please try again later. + '50027': PermissionDenied, # The account is restricted from trading + '50028': ExchangeError, # Unable to take the order, please reach out to support center for details + '50044': BadRequest, # Must select one broker type + '50061': ExchangeError, # You've reached the maximum order rate limit for self account. + '50062': ExchangeError, # This feature is currently unavailable. + # API Class + '50100': ExchangeError, # API frozen, please contact customer service + '50101': AuthenticationError, # Broker id of APIKey does not match current environment + '50102': InvalidNonce, # Timestamp request expired + '50103': AuthenticationError, # Request header "OK_ACCESS_KEY" can not be empty + '50104': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" can not be empty + '50105': AuthenticationError, # Request header "OK_ACCESS_PASSPHRASE" incorrect + '50106': AuthenticationError, # Request header "OK_ACCESS_SIGN" can not be empty + '50107': AuthenticationError, # Request header "OK_ACCESS_TIMESTAMP" can not be empty + '50108': ExchangeError, # Exchange ID does not exist + '50109': ExchangeError, # Exchange domain does not exist + '50110': PermissionDenied, # Invalid IP + '50111': AuthenticationError, # Invalid OK_ACCESS_KEY + '50112': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '50113': AuthenticationError, # Invalid signature + '50114': AuthenticationError, # Invalid authorization + '50115': BadRequest, # Invalid request method + # Trade Class + '51000': BadRequest, # Parameter {0} error + '51001': BadSymbol, # Instrument ID does not exist + '51002': BadSymbol, # Instrument ID does not match underlying index + '51003': BadRequest, # Either client order ID or order ID is required + '51004': InvalidOrder, # Order amount exceeds current tier limit + '51005': InvalidOrder, # Order amount exceeds the limit + '51006': InvalidOrder, # Order price out of the limit + '51007': InvalidOrder, # Order placement failed. Order amount should be at least 1 contract(showing up when placing an order with less than 1 contract) + '51008': InsufficientFunds, # Order placement failed due to insufficient balance + '51009': AccountSuspended, # Order placement function is blocked by the platform + '51010': AccountNotEnabled, # Account level too low {"code":"1","data":[{"clOrdId":"uJrfGFth9F","ordId":"","sCode":"51010","sMsg":"The current account mode does not support self API interface. ","tag":""}],"msg":"Operation failed."} + '51011': InvalidOrder, # Duplicated order ID + '51012': BadSymbol, # Token does not exist + '51014': BadSymbol, # Index does not exist + '51015': BadSymbol, # Instrument ID does not match instrument type + '51016': InvalidOrder, # Duplicated client order ID + '51017': ExchangeError, # Borrow amount exceeds the limit + '51018': ExchangeError, # User with option account can not hold net short positions + '51019': ExchangeError, # No net long positions can be held under isolated margin mode in options + '51020': InvalidOrder, # Order amount should be greater than the min available amount + '51021': ContractUnavailable, # Contract to be listed + '51022': ContractUnavailable, # Contract suspended + '51023': ExchangeError, # Position does not exist + '51024': AccountSuspended, # Unified accountblocked + '51025': ExchangeError, # Order count exceeds the limit + '51026': BadSymbol, # Instrument type does not match underlying index + '51027': ContractUnavailable, # Contract expired + '51028': ContractUnavailable, # Contract under delivery + '51029': ContractUnavailable, # Contract is being settled + '51030': ContractUnavailable, # Funding fee is being settled + '51031': InvalidOrder, # This order price is not within the closing price range + '51046': InvalidOrder, # The take profit trigger price must be higher than the order price + '51047': InvalidOrder, # The stop loss trigger price must be lower than the order price + '51072': InvalidOrder, # As a spot lead trader, you need to set tdMode to 'spot_isolated' when configured buying lead trade pairs + '51073': InvalidOrder, # As a spot lead trader, you need to use '/copytrading/close-subposition' for selling assets through lead trades + '51074': InvalidOrder, # Only the tdMode for lead trade pairs configured by spot lead traders can be set to 'spot_isolated' + '51090': InvalidOrder, # You can't modify the amount of an SL order placed with a TP limit order. + '51091': InvalidOrder, # All TP orders in one order must be of the same type. + '51092': InvalidOrder, # TP order prices(tpOrdPx) in one order must be different. + '51093': InvalidOrder, # TP limit order prices(tpOrdPx) in one order can't be –1(market price). + '51094': InvalidOrder, # You can't place TP limit orders in spot, margin, or options trading. + '51095': InvalidOrder, # To place TP limit orders at self endpoint, you must place an SL order at the same time. + '51096': InvalidOrder, # cxlOnClosePos needs to be True to place a TP limit order + '51098': InvalidOrder, # You can't add a new TP order to an SL order placed with a TP limit order. + '51099': InvalidOrder, # You can't place TP limit orders lead trader. + '51100': InvalidOrder, # Trading amount does not meet the min tradable amount + '51101': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) per transaction + '51102': InvalidOrder, # Entered amount exceeds the max pending count + '51103': InvalidOrder, # Entered amount exceeds the max pending order count of the underlying asset + '51104': InvalidOrder, # Entered amount exceeds the max pending order amount(Cont) of the underlying asset + '51105': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the contract + '51106': InvalidOrder, # Entered amount exceeds the max order amount(Cont) of the underlying asset + '51107': InvalidOrder, # Entered amount exceeds the max holding amount(Cont) + '51108': InvalidOrder, # Positions exceed the limit for closing out with the market price + '51109': InvalidOrder, # No available offer + '51110': InvalidOrder, # You can only place a limit order after Call Auction has started + '51111': BadRequest, # Maximum {0} orders can be placed in bulk + '51112': InvalidOrder, # Close order size exceeds your available size + '51113': RateLimitExceeded, # Market-price liquidation requests too frequent + '51115': InvalidOrder, # Cancel all pending close-orders before liquidation + '51116': InvalidOrder, # Order price or trigger price exceeds {0} + '51117': InvalidOrder, # Pending close-orders count exceeds limit + '51118': InvalidOrder, # Total amount should exceed the min amount per order + '51119': InsufficientFunds, # Order placement failed due to insufficient balance + '51120': InvalidOrder, # Order quantity is less than {0}, please try again + '51121': InvalidOrder, # Order count should be the integer multiples of the lot size + '51122': InvalidOrder, # Order price should be higher than the min price {0} + '51124': InvalidOrder, # You can only place limit orders during call auction + '51125': InvalidOrder, # Currently there are reduce + reverse position pending orders in margin trading. Please cancel all reduce + reverse position pending orders and continue + '51126': InvalidOrder, # Currently there are reduce only pending orders in margin trading.Please cancel all reduce only pending orders and continue + '51127': InsufficientFunds, # Available balance is 0 + '51128': InvalidOrder, # Multi-currency margin account can not do cross-margin trading + '51129': InvalidOrder, # The value of the position and buy order has reached the position limit, and no further buying is allowed + '51130': BadSymbol, # Fixed margin currency error + '51131': InsufficientFunds, # Insufficient balance + '51132': InvalidOrder, # Your position amount is negative and less than the minimum trading amount + '51133': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by multi-currency margin account + '51134': InvalidOrder, # Closing failed. Please check your holdings and pending orders + '51135': InvalidOrder, # Your closing price has triggered the limit price, and the max buy price is {0} + '51136': InvalidOrder, # Your closing price has triggered the limit price, and the min sell price is {0} + '51137': InvalidOrder, # Your opening price has triggered the limit price, and the max buy price is {0} + '51138': InvalidOrder, # Your opening price has triggered the limit price, and the min sell price is {0} + '51139': InvalidOrder, # Reduce-only feature is unavailable for the spot transactions by simple account + '51156': BadRequest, # You're leading trades in long/short mode and can't use self API endpoint to close positions + '51159': BadRequest, # You're leading trades in buy/sell mode. If you want to place orders using self API endpoint, the orders must be in the same direction existing positions and open orders. + '51162': InvalidOrder, # You have {instrument} open orders. Cancel these orders and try again + '51163': InvalidOrder, # You hold {instrument} positions. Close these positions and try again + '51166': InvalidOrder, # Currently, we don't support leading trades with self instrument + '51174': InvalidOrder, # The number of {param0} pending orders reached the upper limit of {param1}(orders). + '51185': InvalidOrder, # The maximum value allowed per order is {maxOrderValue} USD + '51201': InvalidOrder, # Value of per market order cannot exceed 100,000 USDT + '51202': InvalidOrder, # Market - order amount exceeds the max amount + '51203': InvalidOrder, # Order amount exceeds the limit {0} + '51204': InvalidOrder, # The price for the limit order can not be empty + '51205': InvalidOrder, # Reduce-Only is not available + '51250': InvalidOrder, # Algo order price is out of the available range + '51251': InvalidOrder, # Algo order type error(when user place an iceberg order) + '51252': InvalidOrder, # Algo order price is out of the available range + '51253': InvalidOrder, # Average amount exceeds the limit of per iceberg order + '51254': InvalidOrder, # Iceberg average amount error(when user place an iceberg order) + '51255': InvalidOrder, # Limit of per iceberg order: Total amount/1000 < x <= Total amount + '51256': InvalidOrder, # Iceberg order price variance error + '51257': InvalidOrder, # Trail order callback rate error + '51258': InvalidOrder, # Trail - order placement failed. The trigger price of a sell order should be higher than the last transaction price + '51259': InvalidOrder, # Trail - order placement failed. The trigger price of a buy order should be lower than the last transaction price + '51260': InvalidOrder, # Maximum {0} pending trail - orders can be held at the same time + '51261': InvalidOrder, # Each user can hold up to {0} pending stop - orders at the same time + '51262': InvalidOrder, # Maximum {0} pending iceberg orders can be held at the same time + '51263': InvalidOrder, # Maximum {0} pending time-weighted orders can be held at the same time + '51264': InvalidOrder, # Average amount exceeds the limit of per time-weighted order + '51265': InvalidOrder, # Time-weighted order limit error + '51267': InvalidOrder, # Time-weighted order strategy initiative rate error + '51268': InvalidOrder, # Time-weighted order strategy initiative range error + '51269': InvalidOrder, # Time-weighted order interval error, the interval should be {0}<= x<={1} + '51270': InvalidOrder, # The limit of time-weighted order price variance is 0 < x <= 1% + '51271': InvalidOrder, # Sweep ratio should be 0 < x <= 100% + '51272': InvalidOrder, # Price variance should be 0 < x <= 1% + '51273': InvalidOrder, # Total amount should be more than {0} + '51274': InvalidOrder, # Total quantity of time-weighted order must be larger than single order limit + '51275': InvalidOrder, # The amount of single stop-market order can not exceed the upper limit + '51276': InvalidOrder, # Stop - Market orders cannot specify a price + '51277': InvalidOrder, # TP trigger price can not be higher than the last price + '51278': InvalidOrder, # SL trigger price can not be lower than the last price + '51279': InvalidOrder, # TP trigger price can not be lower than the last price + '51280': InvalidOrder, # SL trigger price can not be higher than the last price + '51321': InvalidOrder, # You're leading trades. Currently, we don't support leading trades with arbitrage, iceberg, or TWAP bots + '51322': InvalidOrder, # You're leading trades that have been filled at market price. We've canceled your open stop orders to close your positions + '51323': BadRequest, # You're already leading trades with take profit or stop loss settings. Cancel your existing stop orders to proceed + '51324': BadRequest, # As a lead trader, you hold positions in {instrument}. To close your positions, place orders in the amount that equals the available amount for closing + '51325': InvalidOrder, # As a lead trader, you must use market price when placing stop orders + '51327': InvalidOrder, # closeFraction is only available for futures and perpetual swaps + '51328': InvalidOrder, # closeFraction is only available for reduceOnly orders + '51329': InvalidOrder, # closeFraction is only available in NET mode + '51330': InvalidOrder, # closeFraction is only available for stop market orders + '51400': OrderNotFound, # Cancellation failed order does not exist + '51401': OrderNotFound, # Cancellation failed order is already canceled + '51402': OrderNotFound, # Cancellation failed order is already completed + '51403': InvalidOrder, # Cancellation failed order type does not support cancellation + '51404': InvalidOrder, # Order cancellation unavailable during the second phase of call auction + '51405': ExchangeError, # Cancellation failed do not have any pending orders + '51406': ExchangeError, # Canceled - order count exceeds the limit {0} + '51407': BadRequest, # Either order ID or client order ID is required + '51408': ExchangeError, # Pair ID or name does not match the order info + '51409': ExchangeError, # Either pair ID or pair name ID is required + '51410': CancelPending, # Cancellation failed order is already under cancelling status + '51500': ExchangeError, # Either order price or amount is required + '51501': ExchangeError, # Maximum {0} orders can be modified + '51502': InsufficientFunds, # Order modification failed for insufficient margin + '51503': ExchangeError, # Order modification failed order does not exist + '51506': ExchangeError, # Order modification unavailable for the order type + '51508': ExchangeError, # Orders are not allowed to be modified during the call auction + '51509': ExchangeError, # Modification failed order has been canceled + '51510': ExchangeError, # Modification failed order has been completed + '51511': ExchangeError, # Modification failed order price did not meet the requirement for Post Only + '51600': ExchangeError, # Status not found + '51601': ExchangeError, # Order status and order ID cannot exist at the same time + '51602': ExchangeError, # Either order status or order ID is required + '51603': OrderNotFound, # Order does not exist + '51732': AuthenticationError, # Required user KYC level not met + '51733': AuthenticationError, # User is under risk control + '51734': AuthenticationError, # User KYC Country is not supported + '51735': ExchangeError, # Sub-account is not supported + '51736': InsufficientFunds, # Insufficient {ccy} balance + # Data class + '52000': ExchangeError, # No updates + # SPOT/MARGIN error codes 54000-54999 + '54000': ExchangeError, # Margin transactions unavailable + '54001': ExchangeError, # Only Multi-currency margin account can be set to borrow coins automatically + '54008': InvalidOrder, # This operation is disabled by the 'mass cancel order' endpoint. Please enable it using self endpoint. + '54009': InvalidOrder, # The range of {param0} should be [{param1}, {param2}]. + '54011': InvalidOrder, # 200 Pre-market trading contracts are only allowed to reduce the number of positions within 1 hour before delivery. Please modify or cancel the order. + # Trading bot Error Code from 55100 to 55999 + '55100': InvalidOrder, # Take profit % should be within the range of {parameter1}-{parameter2} + '55101': InvalidOrder, # Stop loss % should be within the range of {parameter1}-{parameter2} + '55102': InvalidOrder, # Take profit % should be greater than the current bot’s PnL% + '55103': InvalidOrder, # Stop loss % should be less than the current bot’s PnL% + '55104': InvalidOrder, # Only futures grid supports take profit or stop loss based on profit percentage + '55111': InvalidOrder, # This signal name is in use, please try a new name + '55112': InvalidOrder, # This signal does not exist + '55113': InvalidOrder, # Create signal strategies with leverage greater than the maximum leverage of the instruments + # FUNDING error codes 58000-58999 + '58000': ExchangeError, # Account type {0} does not supported when getting the sub-account balance + '58001': AuthenticationError, # Incorrect trade password + '58002': PermissionDenied, # Please activate Savings Account first + '58003': ExchangeError, # Currency type is not supported by Savings Account + '58004': AccountSuspended, # Account blocked(transfer & withdrawal endpoint: either end of the account does not authorize the transfer) + '58005': ExchangeError, # The redeemed amount must be no greater than {0} + '58006': ExchangeError, # Service unavailable for token {0} + '58007': ExchangeError, # Abnormal Assets interface. Please try again later + '58100': ExchangeError, # The trading product triggers risk control, and the platform has suspended the fund transfer-out function with related users. Please wait patiently + '58101': AccountSuspended, # Transfer suspended(transfer endpoint: either end of the account does not authorize the transfer) + '58102': RateLimitExceeded, # Too frequent transfer(transfer too frequently) + '58103': ExchangeError, # Parent account user id does not match sub-account user id + '58104': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please contact customer support to remove the restriction + '58105': ExchangeError, # Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please transfer funds on our website or app to complete identity verification + '58106': ExchangeError, # Please enable the account for spot contract + '58107': ExchangeError, # Please enable the account for futures contract + '58108': ExchangeError, # Please enable the account for option contract + '58109': ExchangeError, # Please enable the account for swap contract + '58110': ExchangeError, # The contract triggers risk control, and the platform has suspended the fund transfer function of it. Please wait patiently + '58111': ExchangeError, # Funds transfer unavailable perpetual contract is charging the funding fee. Please try again later + '58112': ExchangeError, # Your fund transfer failed. Please try again later + '58114': ExchangeError, # Transfer amount must be more than 0 + '58115': ExchangeError, # Sub-account does not exist + '58116': ExchangeError, # Transfer amount exceeds the limit + '58117': ExchangeError, # Account assets are abnormal, please deal with negative assets before transferring + '58125': BadRequest, # Non-tradable assets can only be transferred from sub-accounts to main accounts + '58126': BadRequest, # Non-tradable assets can only be transferred between funding accounts + '58127': BadRequest, # Main account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58128': BadRequest, # Sub-account API Key does not support current transfer 'type' parameter. Please refer to the API documentation. + '58200': ExchangeError, # Withdrawal from {0} to {1} is unavailable for self currency + '58201': ExchangeError, # Withdrawal amount exceeds the daily limit + '58202': ExchangeError, # The minimum withdrawal amount for NEO is 1, and the amount must be an integer + '58203': InvalidAddress, # Please add a withdrawal address + '58204': AccountSuspended, # Withdrawal suspended + '58205': ExchangeError, # Withdrawal amount exceeds the upper limit + '58206': ExchangeError, # Withdrawal amount is lower than the lower limit + '58207': InvalidAddress, # Withdrawal failed due to address error + '58208': ExchangeError, # Withdrawal failed. Please link your email + '58209': ExchangeError, # Withdrawal failed. Withdraw feature is not available for sub-accounts + '58210': ExchangeError, # Withdrawal fee exceeds the upper limit + '58211': ExchangeError, # Withdrawal fee is lower than the lower limit(withdrawal endpoint: incorrect fee) + '58212': ExchangeError, # Withdrawal fee should be {0}% of the withdrawal amount + '58213': AuthenticationError, # Please set trading password before withdrawal + '58221': BadRequest, # Missing label of withdrawal address. + '58222': BadRequest, # Illegal withdrawal address. + '58224': BadRequest, # This type of crypto does not support on-chain withdrawing to OKX addresses. Please withdraw through internal transfers. + '58227': BadRequest, # Withdrawal of non-tradable assets can be withdrawn all at once only + '58228': BadRequest, # Withdrawal of non-tradable assets requires that the API Key must be bound to an IP + '58229': InsufficientFunds, # Insufficient funding account balance to pay fees {fee} USDT + '58300': ExchangeError, # Deposit-address count exceeds the limit + '58350': InsufficientFunds, # Insufficient balance + # Account error codes 59000-59999 + '59000': ExchangeError, # Your settings failed have positions or open orders + '59001': ExchangeError, # Switching unavailable have borrowings + '59100': ExchangeError, # You have open positions. Please cancel all open positions before changing the leverage + '59101': ExchangeError, # You have pending orders with isolated positions. Please cancel all the pending orders and adjust the leverage + '59102': ExchangeError, # Leverage exceeds the maximum leverage. Please adjust the leverage + '59103': InsufficientFunds, # Leverage is too low and no sufficient margin in your account. Please adjust the leverage + '59104': ExchangeError, # The leverage is too high. The borrowed position has exceeded the maximum position of self leverage. Please adjust the leverage + '59105': ExchangeError, # Leverage can not be less than {0}. Please adjust the leverage + '59106': ExchangeError, # The max available margin corresponding to your order tier is {0}. Please adjust your margin and place a new order + '59107': ExchangeError, # You have pending orders under the service, please modify the leverage after canceling all pending orders + '59108': InsufficientFunds, # Low leverage and insufficient margin, please adjust the leverage + '59109': ExchangeError, # Account equity less than the required margin amount after adjustment. Please adjust the leverage + '59128': InvalidOrder, # As a lead trader, you can't lead trades in {instrument} with leverage higher than {num} + '59200': InsufficientFunds, # Insufficient account balance + '59201': InsufficientFunds, # Negative account balance + '59216': BadRequest, # The position doesn't exist. Please try again + '59260': PermissionDenied, # You are not a spot lead trader yet. Complete the application on our website or app first. + '59262': PermissionDenied, # You are not a contract lead trader yet. Complete the application on our website or app first. + '59300': ExchangeError, # Margin call failed. Position does not exist + '59301': ExchangeError, # Margin adjustment failed for exceeding the max limit + '59313': ExchangeError, # Unable to repay. You haven't borrowed any {ccy} {ccyPair} in Quick margin mode. + '59401': ExchangeError, # Holdings already reached the limit + '59410': OperationRejected, # You can only borrow self crypto if it supports borrowing and borrowing is enabled. + '59411': InsufficientFunds, # Manual borrowing failed. Your account's free margin is insufficient + '59412': OperationRejected, # Manual borrowing failed. The amount exceeds your borrowing limit. + '59413': OperationRejected, # You didn't borrow self crypto. No repayment needed. + '59414': BadRequest, # Manual borrowing failed. The minimum borrowing limit is {param0}.needed. + '59500': ExchangeError, # Only the APIKey of the main account has permission + '59501': ExchangeError, # Only 50 APIKeys can be created per account + '59502': ExchangeError, # Note name cannot be duplicate with the currently created APIKey note name + '59503': ExchangeError, # Each APIKey can bind up to 20 IP addresses + '59504': ExchangeError, # The sub account does not support the withdrawal function + '59505': ExchangeError, # The passphrase format is incorrect + '59506': ExchangeError, # APIKey does not exist + '59507': ExchangeError, # The two accounts involved in a transfer must be two different sub accounts under the same parent account + '59508': AccountSuspended, # The sub account of {0} is suspended + '59642': BadRequest, # Lead and copy traders can only use margin-free or single-currency margin account modes + '59643': ExchangeError, # Couldn’t switch account modes’re currently copying spot trades + # WebSocket error Codes from 60000-63999 + '60001': AuthenticationError, # "OK_ACCESS_KEY" can not be empty + '60002': AuthenticationError, # "OK_ACCESS_SIGN" can not be empty + '60003': AuthenticationError, # "OK_ACCESS_PASSPHRASE" can not be empty + '60004': AuthenticationError, # Invalid OK_ACCESS_TIMESTAMP + '60005': AuthenticationError, # Invalid OK_ACCESS_KEY + '60006': InvalidNonce, # Timestamp request expired + '60007': AuthenticationError, # Invalid sign + '60008': AuthenticationError, # Login is not supported for public channels + '60009': AuthenticationError, # Login failed + '60010': AuthenticationError, # Already logged in + '60011': AuthenticationError, # Please log in + '60012': BadRequest, # Illegal request + '60013': BadRequest, # Invalid args + '60014': RateLimitExceeded, # Requests too frequent + '60015': NetworkError, # Connection closed was no data transmission in the last 30 seconds + '60016': ExchangeNotAvailable, # Buffer is full, cannot write data + '60017': BadRequest, # Invalid url path + '60018': BadRequest, # The {0} {1} {2} {3} {4} does not exist + '60019': BadRequest, # Invalid op {op} + '60020': ExchangeError, # APIKey subscription amount exceeds the limit + '60021': AccountNotEnabled, # This operation does not support multiple accounts login + '60022': AuthenticationError, # Bulk login partially succeeded + '60023': DDoSProtection, # Bulk login requests too frequent + '60024': AuthenticationError, # Wrong passphrase + '60025': ExchangeError, # Token subscription amount exceeds the limit + '60026': AuthenticationError, # Batch login by APIKey and token simultaneously is not supported + '60027': ArgumentsRequired, # Parameter {0} can not be empty + '60028': NotSupported, # The current operation is not supported by self URL + '60029': AccountNotEnabled, # Only users who are VIP5 and above in trading fee tier are allowed to subscribe to books-l2-tbt channel + '60030': AccountNotEnabled, # Only users who are VIP4 and above in trading fee tier are allowed to subscribe to books50-l2-tbt channel + '60031': AuthenticationError, # The WebSocket endpoint does not support multiple account batch login, + '60032': AuthenticationError, # API key doesn't exist, + '63999': ExchangeError, # Internal system error + '64000': BadRequest, # Subscription parameter uly is unavailable anymore, please replace uly with instFamily. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64001': BadRequest, # This channel has been migrated to the business URL. Please subscribe using the new URL. More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64002': BadRequest, # This channel is not supported by business URL. Please use "/private" URL(for private channels), or "/public" URL(for public channels). More details can refer to: https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url, + '64003': AccountNotEnabled, # Your trading fee tier doesnt meet the requirement to access self channel + '70010': BadRequest, # Timestamp parameters need to be in Unix timestamp format in milliseconds. + '70013': BadRequest, # endTs needs to be bigger than or equal to beginTs. + '70016': BadRequest, # Please specify your instrument settings for at least one instType. + '1009': BadRequest, # Request message exceeds the maximum frame length + '4001': AuthenticationError, # Login Failed + '4002': BadRequest, # Invalid Request + '4003': RateLimitExceeded, # APIKey subscription amount exceeds the limit 100 + '4004': NetworkError, # No data received in 30s + '4005': ExchangeNotAvailable, # Buffer is full, cannot write data + '4006': BadRequest, # Abnormal disconnection + '4007': AuthenticationError, # API key has been updated or deleted. Please reconnect. + '4008': RateLimitExceeded, # The number of subscribed channels exceeds the maximum limit. + }, + 'broad': { + 'Internal Server Error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"} + 'server error': ExchangeNotAvailable, # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"} + }, + }, + 'httpExceptions': { + '429': ExchangeNotAvailable, # https://github.com/ccxt/ccxt/issues/9612 + }, + 'precisionMode': TICK_SIZE, + 'options': { + 'sandboxMode': False, + 'defaultNetwork': 'ERC20', + 'defaultNetworks': { + 'ETH': 'ERC20', + 'BTC': 'BTC', + 'USDT': 'TRC20', + }, + 'networks': { + 'BTC': 'Bitcoin', + 'BTCLN': 'Lightning', + 'BEP20': 'BSC', + 'ERC20': 'ERC20', + 'TRC20': 'TRC20', + 'CRC20': 'Crypto', + # sorted + 'ACA': 'Acala', + 'ALGO': 'Algorand', + 'BHP': 'BHP', + 'APT': 'Aptos', + 'ARBONE': 'Arbitrum One', + 'AVAXC': 'Avalanche C-Chain', + 'AVAXX': 'Avalanche X-Chain', + 'ARK': 'ARK', + 'AR': 'Arweave', + 'ASTR': 'Astar', + 'BCH': 'BitcoinCash', + 'BSV': 'Bitcoin SV', + 'BTM': 'Bytom', + 'ADA': 'Cardano', + 'CSPR': 'Casper', + 'CELO': 'CELO', + 'XCH': 'Chia', + 'CHZ': 'Chiliz', + 'ATOM': 'Cosmos', + 'TRUE': 'TrueChain', + 'DCR': 'Decred', + 'DGB': 'Digibyte', + 'DOGE': 'Dogecoin', + 'XEC': 'XEC', + 'EGLD': 'Elrond', + 'EOS': 'EOS', + 'ETC': 'Ethereum Classic', + 'ETHW': 'EthereumPow', + 'FTM': 'Fantom', + 'FIL': 'Filecoin', + 'FLOW': 'FLOW', + 'FSN': 'Fusion', + 'ONE': 'Harmony', + 'HBAR': 'Hedera', + 'HNT': 'Helium', + 'ZEN': 'Horizen', + 'ICX': 'ICON', + 'ICP': 'Dfinity', + 'IOST': 'IOST', + 'IOTA': 'MIOTA', + 'KDA': 'Kadena', + 'KAR': 'KAR', + 'KLAY': 'Klaytn', + 'KSM': 'Kusama', + 'LSK': 'Lisk', + 'LTC': 'Litecoin', + 'METIS': 'Metis', + 'MINA': 'Mina', + 'XMR': 'Monero', + 'GLRM': 'Moonbeam', + 'MOVR': 'Moonriver', + 'NANO': 'Nano', + 'NEAR': 'NEAR', + 'NAS': 'Nebulas', + 'NEM': 'New Economy Movement', + 'NULS': 'NULS', + 'OASYS': 'OASYS', + 'OKC': 'OKC', + 'ONT': 'Ontology', + 'OPTIMISM': 'Optimism', + 'LAT': 'PlatON', + 'DOT': 'Polkadot', + 'MATIC': 'Polygon', + 'RVN': 'Ravencoin', + 'XRP': 'Ripple', + 'SC': 'Siacoin', + 'SOL': 'Solana', + 'STX': 'l-Stacks', + 'XLM': 'Stellar Lumens', + 'XTZ': 'Tezos', + 'TON': 'TON', + 'THETA': 'Theta', + 'VSYS': 'VSYSTEMS', + 'WAVES': 'WAVES', + 'WAX': 'Wax', + 'ZEC': 'Zcash', + 'ZIL': 'Zilliqa', + 'ZKSYNC': 'ZKSYNC', + 'OMNI': 'Omni', + # 'NEON3': 'N3', # tbd + # undetermined : "CELO-TOKEN", "Digital Cash", Khala + # todo: uncomment below after consensus + # 'AELF': 'AELF', + # 'BITCOINDIAMOND': 'Bitcoin Diamond', + # 'BITCOINGOLD': 'BitcoinGold', + # 'YOYOW': 'YOYOW', + # 'QTUM': 'Quantum', + # 'INTCHAIN': 'INTCHAIN', + # 'YOUCHAIN': 'YOUCHAIN', + # 'RONIN': 'Ronin', + # 'OEC': 'OEC', + # 'WAYIKICHAIN': 'WGRT', + # 'MDNA': 'DNA', + # 'STEP': 'Step Network', + # 'EMINER': 'Eminer', + # 'CYBERMILES': 'CyberMiles', + # 'HYPERCASH': 'HyperCash', + # 'CONFLUX': 'Conflux', + # 'CORTEX': 'Cortex', + # 'TERRA': 'Terra', + # 'TERRACLASSIC': 'Terra Classic', + }, + 'fetchOpenInterestHistory': { + 'timeframes': { + '5m': '5m', + '1h': '1H', + '8h': '8H', + '1d': '1D', + '5M': '5m', + '1H': '1H', + '8H': '8H', + '1D': '1D', + }, + }, + 'fetchOHLCV': { + # 'type': 'Candles', # Candles or HistoryCandles, IndexCandles, MarkPriceCandles + 'timezone': 'UTC', # UTC, HK + }, + 'fetchPositions': { + 'method': 'privateGetAccountPositions', # privateGetAccountPositions or privateGetAccountPositionsHistory + }, + 'createOrder': 'privatePostTradeBatchOrders', # or 'privatePostTradeOrder' or 'privatePostTradeOrderAlgo' + 'createMarketBuyOrderRequiresPrice': False, + 'fetchMarkets': ['spot', 'future', 'swap', 'option'], # spot, future, swap, option + 'timeDifference': 0, # the difference between system clock and exchange server clock + 'adjustForTimeDifference': False, # controls the adjustment logic upon instantiation + 'defaultType': 'spot', # 'funding', 'spot', 'margin', 'future', 'swap', 'option' + # 'fetchBalance': { + # 'type': 'spot', # 'funding', 'trading', 'spot' + # }, + 'fetchLedger': { + 'method': 'privateGetAccountBills', # privateGetAccountBills, privateGetAccountBillsArchive, privateGetAssetBills + }, + # 6: Funding account, 18: Trading account + 'fetchOrder': { + 'method': 'privateGetTradeOrder', # privateGetTradeOrdersAlgoHistory + }, + 'fetchOpenOrders': { + 'method': 'privateGetTradeOrdersPending', # privateGetTradeOrdersAlgoPending + }, + 'cancelOrders': { + 'method': 'privatePostTradeCancelBatchOrders', # privatePostTradeCancelAlgos + }, + 'fetchCanceledOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'fetchClosedOrders': { + 'method': 'privateGetTradeOrdersHistory', # privateGetTradeOrdersAlgoHistory + }, + 'withdraw': { + # a funding password credential is required by the exchange for the + # withdraw call(not to be confused with the api password credential) + 'password': None, + 'pwd': None, # password or pwd both work + }, + 'algoOrderTypes': { + 'conditional': True, + 'trigger': True, + 'oco': True, + 'move_order_stop': True, + 'iceberg': True, + 'twap': True, + }, + 'accountsByType': { + 'funding': '6', + 'trading': '18', # unified trading account + 'spot': '18', + 'future': '18', + 'futures': '18', + 'margin': '18', + 'swap': '18', + 'option': '18', + }, + 'accountsById': { + '6': 'funding', + '18': 'trading', # unified trading account + }, + 'exchangeType': { + 'spot': 'SPOT', + 'margin': 'MARGIN', + 'swap': 'SWAP', + 'future': 'FUTURES', + 'futures': 'FUTURES', # deprecated + 'option': 'OPTION', + 'SPOT': 'SPOT', + 'MARGIN': 'MARGIN', + 'SWAP': 'SWAP', + 'FUTURES': 'FUTURES', + 'OPTION': 'OPTION', + }, + 'brokerId': 'e847386590ce4dBC', + }, + 'features': { + 'default': { + 'sandbox': True, + 'createOrder': { + 'marginMode': True, + 'triggerPrice': True, + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'triggerDirection': False, + 'stopLossPrice': True, + 'takeProfitPrice': True, + 'attachedStopLossTakeProfit': { + 'triggerPriceType': { + 'last': True, + 'mark': True, + 'index': True, + }, + 'price': True, + }, + 'timeInForce': { + 'IOC': True, + 'FOK': True, + 'PO': True, + 'GTD': False, + }, + 'hedged': True, + 'trailing': True, + 'iceberg': True, # todo implement + 'leverage': False, + 'selfTradePrevention': True, # todo implement + 'marketBuyByCost': True, + 'marketBuyRequiresPrice': False, + }, + 'createOrders': { + 'max': 20, + }, + 'fetchMyTrades': { + 'marginMode': False, + 'daysBack': 90, + 'limit': 100, + 'untilDays': 10000, + 'symbolRequired': False, + }, + 'fetchOrder': { + 'marginMode': False, + 'trigger': True, + 'trailing': True, + 'symbolRequired': True, + }, + 'fetchOpenOrders': { + 'marginMode': False, + 'limit': 100, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOrders': None, # not supported + 'fetchClosedOrders': { + 'marginMode': False, + 'limit': 100, + 'daysBack': 90, # 3 months + 'daysBackCanceled': 1 / 12, # 2 hour + 'untilDays': None, + 'trigger': True, + 'trailing': True, + 'symbolRequired': False, + }, + 'fetchOHLCV': { + 'limit': 300, + }, + }, + 'spot': { + 'extends': 'default', + }, + 'swap': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + 'future': { + 'linear': { + 'extends': 'default', + }, + 'inverse': { + 'extends': 'default', + }, + }, + }, + 'commonCurrencies': { + # the exchange refers to ERC20 version of Aeternity(AEToken) + 'AE': 'AET', # https://github.com/ccxt/ccxt/issues/4981 + 'WIN': 'WINTOKEN', # https://github.com/ccxt/ccxt/issues/5701 + }, + }) + + def handle_market_type_and_params(self, methodName: str, market: Market = None, params={}, defaultValue=None) -> Any: + instType = self.safe_string(params, 'instType') + params = self.omit(params, 'instType') + type = self.safe_string(params, 'type') + if (type is None) and (instType is not None): + params['type'] = instType + return super(okx, self).handle_market_type_and_params(methodName, market, params, defaultValue) + + def convert_to_instrument_type(self, type): + exchangeTypes = self.safe_dict(self.options, 'exchangeType', {}) + return self.safe_string(exchangeTypes, type, type) + + def create_expired_option_market(self, symbol: str): + # support expired option contracts + quote = 'USD' + optionParts = symbol.split('-') + symbolBase = symbol.split('/') + base = None + if symbol.find('/') > -1: + base = self.safe_string(symbolBase, 0) + else: + base = self.safe_string(optionParts, 0) + settle = base + expiry = self.safe_string(optionParts, 2) + strike = self.safe_string(optionParts, 3) + optionType = self.safe_string(optionParts, 4) + datetime = self.convert_expire_date(expiry) + timestamp = self.parse8601(datetime) + return { + 'id': base + '-' + quote + '-' + expiry + '-' + strike + '-' + optionType, + 'symbol': base + '/' + quote + ':' + settle + '-' + expiry + '-' + strike + '-' + optionType, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settle, + 'active': False, + 'type': 'option', + 'linear': None, + 'inverse': None, + 'spot': False, + 'swap': False, + 'future': False, + 'option': True, + 'margin': False, + 'contract': True, + 'contractSize': self.parse_number('1'), + 'expiry': timestamp, + 'expiryDatetime': datetime, + 'optionType': 'call' if (optionType == 'C') else 'put', + 'strike': self.parse_number(strike), + 'precision': { + 'amount': None, + 'price': None, + }, + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + }, + }, + 'info': None, + } + + def safe_market(self, marketId: Str = None, market: Market = None, delimiter: Str = None, marketType: Str = None) -> MarketInterface: + isOption = (marketId is not None) and ((marketId.find('-C') > -1) or (marketId.find('-P') > -1)) + if isOption and not (marketId in self.markets_by_id): + # handle expired option contracts + return self.create_expired_option_market(marketId) + return super(okx, self).safe_market(marketId, market, delimiter, marketType) + + async def fetch_status(self, params={}): + """ + the latest known information on the availability of the exchange API + + https://www.okx.com/docs-v5/en/#status-get-status + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `status structure ` + """ + response = await self.publicGetSystemStatus(params) + # + # Note, if there is no maintenance around, the 'data' array is empty + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "begin": "1621328400000", + # "end": "1621329000000", + # "href": "https://www.okx.com/support/hc/en-us/articles/360060882172", + # "scheDesc": "", + # "serviceType": "1", # 0 WebSocket, 1 Spot/Margin, 2 Futures, 3 Perpetual, 4 Options, 5 Trading service + # "state": "scheduled", # ongoing, completed, canceled + # "system": "classic", # classic, unified + # "title": "Classic Spot System Upgrade" + # }, + # ] + # } + # + data = self.safe_list(response, 'data', []) + dataLength = len(data) + update: dict = { + 'updated': None, + 'status': 'ok' if (dataLength == 0) else 'maintenance', + 'eta': None, + 'url': None, + 'info': response, + } + for i in range(0, len(data)): + event = data[i] + state = self.safe_string(event, 'state') + update['eta'] = self.safe_integer(event, 'end') + update['url'] = self.safe_string(event, 'href') + if state == 'ongoing': + update['status'] = 'maintenance' + elif state == 'scheduled': + update['status'] = 'ok' + elif state == 'completed': + update['status'] = 'ok' + elif state == 'canceled': + update['status'] = 'ok' + return update + + async def fetch_time(self, params={}) -> Int: + """ + fetches the current integer timestamp in milliseconds from the exchange server + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-system-time + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns int: the current integer timestamp in milliseconds from the exchange server + """ + response = await self.publicGetPublicTime(params) + # + # { + # "code": "0", + # "data": [ + # {"ts": "1621247923668"} + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.safe_integer(first, 'ts') + + async def fetch_accounts(self, params={}) -> List[Account]: + """ + fetch all the accounts associated with a profile + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `account structures ` indexed by the account type + """ + response = await self.privateGetAccountConfig(params) + # + # { + # "code": "0", + # "data": [ + # { + # "acctLv": "2", + # "autoLoan": False, + # "ctIsoMode": "automatic", + # "greeksType": "PA", + # "level": "Lv1", + # "levelTmp": "", + # "mgnIsoMode": "automatic", + # "posMode": "long_short_mode", + # "uid": "88018754289672195" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + account = data[i] + accountId = self.safe_string(account, 'uid') + type = self.safe_string(account, 'acctLv') + result.append({ + 'id': accountId, + 'type': type, + 'currency': None, + 'info': account, + 'code': None, + }) + return result + + def nonce(self): + return self.milliseconds() - self.options['timeDifference'] + + async def fetch_markets(self, params={}) -> List[Market]: + """ + retrieves data on all markets for okx + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of objects representing market data + """ + if self.options['adjustForTimeDifference']: + await self.load_time_difference() + types = self.safe_list(self.options, 'fetchMarkets', []) + promises = [] + result = [] + for i in range(0, len(types)): + promises.append(self.fetch_markets_by_type(types[i], params)) + promises = await asyncio.gather(*promises) + for i in range(0, len(promises)): + result = self.array_concat(result, promises[i]) + return result + + def parse_market(self, market: dict) -> Market: + base = self.safe_string(market, 'baseCcy') + quote = self.safe_string(market, 'quoteCcy') + inst_id = self.safe_string(market, 'instId') + if not base or not quote or base == '' or quote == '': + self.logger.warning(f"Skipping invalid market: instId={inst_id}, baseCcy={base}, quoteCcy={quote}") + return None + symbol = base + '/' + quote + id = self.safe_string(market, 'instId') + type = self.safe_string(market, 'instType') + spot = type == 'SPOT' + margin = type == 'MARGIN' + swap = type == 'SWAP' + future = type == 'FUTURES' + option = type == 'OPTION' + contract = swap or future or option + settle = self.safe_string(market, 'settleCcy') + settleId = self.safe_string(market, 'settleCcy') + return { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': base, + 'quoteId': quote, + 'settleId': settleId, + 'type': type.lower(), + 'spot': spot, + 'margin': margin, + 'swap': swap, + 'future': future, + 'option': option, + 'contract': contract, + 'active': True, + 'info': market + } + def parse_market(self, market: dict) -> Market: + # + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # + # { + # "alias": "", + # "baseCcy": "", + # "category": "1", + # "ctMult": "0.1", + # "ctType": "", + # "ctVal": "1", + # "ctValCcy": "BTC", + # "expTime": "1648195200000", + # "instId": "BTC-USD-220325-194000-P", + # "instType": "OPTION", + # "lever": "", + # "listTime": "1631262612280", + # "lotSz": "1", + # "minSz": "1", + # "optType": "P", + # "quoteCcy": "", + # "settleCcy": "BTC", + # "state": "live", + # "stk": "194000", + # "tickSz": "0.0005", + # "uly": "BTC-USD" + # } + # + id = self.safe_string(market, 'instId') + type = self.safe_string_lower(market, 'instType') + if type == 'futures': + type = 'future' + spot = (type == 'spot') + future = (type == 'future') + swap = (type == 'swap') + option = (type == 'option') + contract = swap or future or option + baseId = self.safe_string(market, 'baseCcy') + quoteId = self.safe_string(market, 'quoteCcy') + settleId = self.safe_string(market, 'settleCcy') + settle = self.safe_currency_code(settleId) + underlying = self.safe_string(market, 'uly') + if (underlying is not None) and not spot: + parts = underlying.split('-') + baseId = self.safe_string(parts, 0) + quoteId = self.safe_string(parts, 1) + base = self.safe_currency_code(baseId) + quote = self.safe_currency_code(quoteId) + symbol = base + '/' + quote + expiry = None + strikePrice = None + optionType = None + if contract: + symbol = symbol + ':' + settle + expiry = self.safe_integer(market, 'expTime') + if future: + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + elif option: + strikePrice = self.safe_string(market, 'stk') + optionType = self.safe_string(market, 'optType') + ymd = self.yymmdd(expiry) + symbol = symbol + '-' + ymd + '-' + strikePrice + '-' + optionType + optionType = 'put' if (optionType == 'P') else 'call' + tickSize = self.safe_string(market, 'tickSz') + fees = self.safe_dict_2(self.fees, type, 'trading', {}) + maxLeverage = self.safe_string(market, 'lever', '1') + maxLeverage = Precise.string_max(maxLeverage, '1') + maxSpotCost = self.safe_number(market, 'maxMktSz') + return self.extend(fees, { + 'id': id, + 'symbol': symbol, + 'base': base, + 'quote': quote, + 'settle': settle, + 'baseId': baseId, + 'quoteId': quoteId, + 'settleId': settleId, + 'type': type, + 'spot': spot, + 'margin': spot and (Precise.string_gt(maxLeverage, '1')), + 'swap': swap, + 'future': future, + 'option': option, + 'active': True, + 'contract': contract, + 'linear': (quoteId == settleId) if contract else None, + 'inverse': (baseId == settleId) if contract else None, + 'contractSize': self.safe_number(market, 'ctVal') if contract else None, + 'expiry': expiry, + 'expiryDatetime': self.iso8601(expiry), + 'strike': self.parse_number(strikePrice), + 'optionType': optionType, + 'created': self.safe_integer(market, 'listTime'), + 'precision': { + 'amount': self.safe_number(market, 'lotSz'), + 'price': self.parse_number(tickSize), + }, + 'limits': { + 'leverage': { + 'min': self.parse_number('1'), + 'max': self.parse_number(maxLeverage), + }, + 'amount': { + 'min': self.safe_number(market, 'minSz'), + 'max': None, + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None if contract else maxSpotCost, + }, + }, + 'info': market, + }) + + async def fetch_markets_by_type(self, type, params={}): + request: dict = { + 'instType': self.convert_to_instrument_type(type), + } + if type == 'option': + optionsUnderlying = self.safe_list(self.options, 'defaultUnderlying', ['BTC-USD', 'ETH-USD']) + promises = [] + for i in range(0, len(optionsUnderlying)): + underlying = optionsUnderlying[i] + request['uly'] = underlying + promises.append(self.publicGetPublicInstruments(self.extend(request, params))) + promisesResult = await asyncio.gather(*promises) + markets = [] + for i in range(0, len(promisesResult)): + res = self.safe_dict(promisesResult, i, {}) + options = self.safe_list(res, 'data', []) + markets = self.array_concat(markets, options) + return self.parse_markets(markets) + response = await self.publicGetPublicInstruments(self.extend(request, params)) + # + # spot, future, swap, option + # + # { + # "code": "0", + # "data": [ + # { + # "alias": "", # self_week, next_week, quarter, next_quarter + # "baseCcy": "BTC", + # "category": "1", + # "ctMult": "", + # "ctType": "", # inverse, linear + # "ctVal": "", + # "ctValCcy": "", + # "expTime": "", + # "instId": "BTC-USDT", # BTC-USD-210521, CSPR-USDT-SWAP, BTC-USD-210517-44000-C + # "instType": "SPOT", # SPOT, FUTURES, SWAP, OPTION + # "lever": "10", + # "listTime": "1548133413000", + # "lotSz": "0.00000001", + # "minSz": "0.00001", + # "optType": "", + # "quoteCcy": "USDT", + # "settleCcy": "", + # "state": "live", + # "stk": "", + # "tickSz": "0.1", + # "uly": "" + # } + # ], + # "msg": "" + # } + # + dataResponse = self.safe_list(response, 'data', []) + return self.parse_markets(dataResponse) + + def parse_markets(self, markets): + result = [] + for i in range(0, len(markets)): + parsed = self.parse_market(markets[i]) + if parsed is not None: + result.append(parsed) + return result + + async def fetch_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies on an exchange + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + # self endpoint requires authentication + # while fetchCurrencies is a public API method by design + # therefore we check the keys here + # and fallback to generating the currencies from the markets + isSandboxMode = self.safe_bool(self.options, 'sandboxMode', False) + if not self.check_required_credentials(False) or isSandboxMode: + return None + # + # has['fetchCurrencies'] is currently set to True, but an unauthorized request returns + # + # {"msg":"Request header “OK_ACCESS_KEY“ can't be empty.","code":"50103"} + # + response = await self.privateGetAssetCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result: dict = {} + dataByCurrencyId = self.group_by(data, 'ccy') + currencyIds = list(dataByCurrencyId.keys()) + for i in range(0, len(currencyIds)): + currencyId = currencyIds[i] + currency = self.safe_currency(currencyId) + code = currency['code'] + chains = dataByCurrencyId[currencyId] + networks: dict = {} + currencyActive = False + depositEnabled = False + withdrawEnabled = False + maxPrecision = None + for j in range(0, len(chains)): + chain = chains[j] + canDeposit = self.safe_bool(chain, 'canDep') + depositEnabled = canDeposit if (canDeposit) else depositEnabled + canWithdraw = self.safe_bool(chain, 'canWd') + withdrawEnabled = canWithdraw if (canWithdraw) else withdrawEnabled + canInternal = self.safe_bool(chain, 'canInternal') + active = True if (canDeposit and canWithdraw and canInternal) else False + currencyActive = active if (active) else currencyActive + networkId = self.safe_string(chain, 'chain') + if (networkId is not None) and (networkId.find('-') >= 0): + idParts = networkId.split('-') + parts = self.array_slice(idParts, 1) + chainPart = '-'.join(parts) + networkCode = self.network_id_to_code(chainPart, currency['code']) + precision = self.parse_precision(self.safe_string(chain, 'wdTickSz')) + if maxPrecision is None: + maxPrecision = precision + else: + maxPrecision = Precise.string_min(maxPrecision, precision) + networks[networkCode] = { + 'id': networkId, + 'network': networkCode, + 'active': active, + 'deposit': canDeposit, + 'withdraw': canWithdraw, + 'fee': self.safe_number(chain, 'fee'), + 'precision': self.parse_number(precision), + 'limits': { + 'withdraw': { + 'min': self.safe_number(chain, 'minWd'), + 'max': self.safe_number(chain, 'maxWd'), + }, + }, + 'info': chain, + } + firstChain = self.safe_dict(chains, 0, {}) + result[code] = { + 'info': chains, + 'code': code, + 'id': currencyId, + 'name': self.safe_string(firstChain, 'name'), + 'active': currencyActive, + 'deposit': depositEnabled, + 'withdraw': withdrawEnabled, + 'fee': None, + 'precision': self.parse_number(maxPrecision), + 'limits': { + 'amount': { + 'min': None, + 'max': None, + }, + }, + 'networks': networks, + } + return result + + async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: + """ + fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-order-book + + :param str symbol: unified symbol of the market to fetch the order book for + :param int [limit]: the maximum amount of order book entries to return + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketBooksFull' or 'publicGetMarketBooks' default is 'publicGetMarketBooks' + :returns dict: A dictionary of `order book structures ` indexed by market symbols + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + method = None + method, params = self.handle_option_and_params(params, 'fetchOrderBook', 'method', 'publicGetMarketBooks') + if method == 'publicGetMarketBooksFull' and limit is None: + limit = 5000 + limit = 100 if (limit is None) else limit + if limit is not None: + request['sz'] = limit # max 400 + response = None + if (method == 'publicGetMarketBooksFull') or (limit > 400): + response = await self.publicGetMarketBooksFull(self.extend(request, params)) + else: + response = await self.publicGetMarketBooks(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "asks": [ + # ["0.07228","4.211619","0","2"], # price, amount, liquidated orders, total open orders + # ["0.0723","299.880364","0","2"], + # ["0.07231","3.72832","0","1"], + # ], + # "bids": [ + # ["0.07221","18.5","0","1"], + # ["0.0722","18.5","0","1"], + # ["0.07219","0.505407","0","1"], + # ], + # "ts": "1621438475342" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'ts') + return self.parse_order_book(first, symbol, timestamp) + + def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: + # + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "markPx":"200", + # "ts":"1597026383085" + # } + # + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # { + # instId: 'LTC-USDT', + # idxPx: '65.74', + # open24h: '65.37', + # high24h: '66.15', + # low24h: '64.97', + # sodUtc0: '65.68', + # sodUtc8: '65.54', + # ts: '1728467346900' + # }, + # + timestamp = self.safe_integer(ticker, 'ts') + marketId = self.safe_string(ticker, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + last = self.safe_string(ticker, 'last') + open = self.safe_string(ticker, 'open24h') + spot = self.safe_bool(market, 'spot', False) + quoteVolume = self.safe_string(ticker, 'volCcy24h') if spot else None + baseVolume = self.safe_string(ticker, 'vol24h') + high = self.safe_string(ticker, 'high24h') + low = self.safe_string(ticker, 'low24h') + return self.safe_ticker({ + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'high': high, + 'low': low, + 'bid': self.safe_string(ticker, 'bidPx'), + 'bidVolume': self.safe_string(ticker, 'bidSz'), + 'ask': self.safe_string(ticker, 'askPx'), + 'askVolume': self.safe_string(ticker, 'askSz'), + 'vwap': None, + 'open': open, + 'close': last, + 'last': last, + 'previousClose': None, + 'change': None, + 'percentage': None, + 'average': None, + 'baseVolume': baseVolume, + 'quoteVolume': quoteVolume, + 'markPrice': self.safe_string(ticker, 'markPx'), + 'indexPrice': self.safe_string(ticker, 'idxPx'), + 'info': ticker, + }, market) + + async def fetch_ticker(self, symbol: str, params={}) -> Ticker: + """ + fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `ticker structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "ETH-BTC", + # "last": "0.07319", + # "lastSz": "0.044378", + # "askPx": "0.07322", + # "askSz": "4.2", + # "bidPx": "0.0732", + # "bidSz": "6.050058", + # "open24h": "0.07801", + # "high24h": "0.07975", + # "low24h": "0.06019", + # "volCcy24h": "11788.887619", + # "vol24h": "167493.829229", + # "ts": "1621440583784", + # "sodUtc0": "0.07872", + # "sodUtc8": "0.07345" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_ticker(first, market) + + async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params) + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchTickers() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = await self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "SPOT", + # "instId": "BCD-BTC", + # "last": "0.0000769", + # "lastSz": "5.4788", + # "askPx": "0.0000777", + # "askSz": "3.2197", + # "bidPx": "0.0000757", + # "bidSz": "4.7509", + # "open24h": "0.0000885", + # "high24h": "0.0000917", + # "low24h": "0.0000596", + # "volCcy24h": "9.2877", + # "vol24h": "124824.1985", + # "ts": "1621441741434", + # "sodUtc0": "0.0000905", + # "sodUtc8": "0.0000729" + # }, + # ] + # } + # + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + async def fetch_mark_price(self, symbol: str, params={}) -> Ticker: + """ + fetches mark price for the market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str symbol: unified symbol of the market to fetch the ticker for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetPublicMarkPrice(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "ETH-USDT", + # "instType": "MARGIN", + # "markPx": "2403.98", + # "ts": "1728578500703" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_ticker(self.safe_dict(data, 0), market) + + async def fetch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers: + """ + fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-mark-price + + :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `ticker structures ` + """ + await self.load_markets() + symbols = self.market_symbols(symbols) + market = self.get_market_from_symbols(symbols) + marketType = None + marketType, params = self.handle_market_type_and_params('fetchTickers', market, params, 'swap') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + if marketType == 'option': + defaultUnderlying = self.safe_string(self.options, 'defaultUnderlying', 'BTC-USD') + currencyId = self.safe_string_2(params, 'uly', 'marketId', defaultUnderlying) + if currencyId is None: + raise ArgumentsRequired(self.id + ' fetchMarkPrices() requires an underlying uly or marketId parameter for options markets') + else: + request['uly'] = currencyId + response = await self.publicGetPublicMarkPrice(self.extend(request, params)) + tickers = self.safe_list(response, 'data', []) + return self.parse_tickers(tickers, symbols) + + def parse_trade(self, trade: dict, market: Market = None) -> Trade: + # + # public fetchTrades + # + # { + # "instId": "ETH-BTC", + # "side": "sell", + # "sz": "0.119501", + # "px": "0.07065", + # "tradeId": "15826757", + # "ts": "1621446178316" + # } + # + # option: fetchTrades + # + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # } + # + # private fetchMyTrades + # + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # + id = self.safe_string(trade, 'tradeId') + marketId = self.safe_string(trade, 'instId') + market = self.safe_market(marketId, market, '-') + symbol = market['symbol'] + timestamp = self.safe_integer(trade, 'ts') + price = self.safe_string_2(trade, 'fillPx', 'px') + amount = self.safe_string_2(trade, 'fillSz', 'sz') + side = self.safe_string(trade, 'side') + orderId = self.safe_string(trade, 'ordId') + feeCostString = self.safe_string(trade, 'fee') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(trade, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': feeCostSigned, + 'currency': feeCurrencyCode, + } + takerOrMaker = self.safe_string(trade, 'execType') + if takerOrMaker == 'T': + takerOrMaker = 'taker' + elif takerOrMaker == 'M': + takerOrMaker = 'maker' + return self.safe_trade({ + 'info': trade, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'symbol': symbol, + 'id': id, + 'order': orderId, + 'type': None, + 'takerOrMaker': takerOrMaker, + 'side': side, + 'price': price, + 'amount': amount, + 'cost': None, + 'fee': fee, + }, market) + + async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: + """ + get the list of most recent trades for a particular symbol + + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-trades + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-option-trades + + :param str symbol: unified symbol of the market to fetch trades for + :param int [since]: timestamp in ms of the earliest trade to fetch + :param int [limit]: the maximum amount of trades to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.method]: 'publicGetMarketTrades' or 'publicGetMarketHistoryTrades' default is 'publicGetMarketTrades' + :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = None + if market['option']: + response = await self.publicGetPublicOptionTrades(self.extend(request, params)) + else: + if limit is not None: + request['limit'] = limit # default 100 + method = None + method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades') + if method == 'publicGetMarketTrades': + response = await self.publicGetMarketTrades(self.extend(request, params)) + elif method == 'publicGetMarketHistoryTrades': + response = await self.publicGetMarketHistoryTrades(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # {"instId":"ETH-BTC","side":"sell","sz":"0.119501","px":"0.07065","tradeId":"15826757","ts":"1621446178316"}, + # {"instId":"ETH-BTC","side":"sell","sz":"0.03","px":"0.07068","tradeId":"15826756","ts":"1621446178066"}, + # {"instId":"ETH-BTC","side":"buy","sz":"0.507","px":"0.07069","tradeId":"15826755","ts":"1621446175085"}, + # ] + # } + # + # option + # + # { + # "code": "0", + # "data": [ + # { + # "fillVol": "0.46387625976562497", + # "fwdPx": "26299.754935451125", + # "indexPx": "26309.7", + # "instFamily": "BTC-USD", + # "instId": "BTC-USD-230526-26000-C", + # "markPx": "0.042386283557554236", + # "optType": "C", + # "px": "0.0415", + # "side": "sell", + # "sz": "90", + # "tradeId": "112", + # "ts": "1683907480154" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit) + + def parse_ohlcv(self, ohlcv, market: Market = None) -> list: + # + # [ + # "1678928760000", # timestamp + # "24341.4", # open + # "24344", # high + # "24313.2", # low + # "24323", # close + # "628", # contract volume + # "2.5819", # base volume + # "62800", # quote volume + # "0" # candlestick state + # ] + # + res = self.handle_market_type_and_params('fetchOHLCV', market, None) + type = res[0] + volumeIndex = 5 if (type == 'spot') else 6 + return [ + self.safe_integer(ohlcv, 0), + self.safe_number(ohlcv, 1), + self.safe_number(ohlcv, 2), + self.safe_number(ohlcv, 3), + self.safe_number(ohlcv, 4), + self.safe_number(ohlcv, volumeIndex), + ] + + async def fetch_ohlcv(self, symbol: str, timeframe='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://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-mark-price-candlesticks-history + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks + https://www.okx.com/docs-v5/en/#rest-api-market-data-get-index-candlesticks-history + + :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 str [params.price]: "mark" or "index" for mark price and index price candles + :param int [params.until]: timestamp in ms of the latest candle to fetch + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns int[][]: A list of candles ordered, open, high, low, close, volume + """ + await self.load_markets() + market = self.market(symbol) + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 200) + price = self.safe_string(params, 'price') + params = self.omit(params, 'price') + options = self.safe_dict(self.options, 'fetchOHLCV', {}) + timezone = self.safe_string(options, 'timezone', 'UTC') + if limit is None: + limit = 100 # default 100, max 100 + duration = self.parse_timeframe(timeframe) + bar = self.safe_string(self.timeframes, timeframe, timeframe) + if (timezone == 'UTC') and (duration >= 21600): # if utc and timeframe >= 6h + bar += timezone.lower() + request: dict = { + 'instId': market['id'], + 'bar': bar, + 'limit': limit, + } + defaultType = 'Candles' + if since is not None: + now = self.milliseconds() + durationInMilliseconds = duration * 1000 + # switch to history candles if since is past the cutoff for current candles + historyBorder = now - ((1440 - 1) * durationInMilliseconds) + if since < historyBorder: + defaultType = 'HistoryCandles' + startTime = max(since - 1, 0) + request['before'] = startTime + request['after'] = self.sum(since, durationInMilliseconds * limit) + until = self.safe_integer(params, 'until') + if until is not None: + request['after'] = until + params = self.omit(params, 'until') + defaultType = self.safe_string(options, 'type', defaultType) # Candles or HistoryCandles + type = self.safe_string(params, 'type', defaultType) + params = self.omit(params, 'type') + isHistoryCandles = (type == 'HistoryCandles') + response = None + if price == 'mark': + if isHistoryCandles: + response = await self.publicGetMarketHistoryMarkPriceCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketMarkPriceCandles(self.extend(request, params)) + elif price == 'index': + request['instId'] = market['info']['instFamily'] # okx index candles require instFamily instead of instId + if isHistoryCandles: + response = await self.publicGetMarketHistoryIndexCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketIndexCandles(self.extend(request, params)) + else: + if isHistoryCandles: + response = await self.publicGetMarketHistoryCandles(self.extend(request, params)) + else: + response = await self.publicGetMarketCandles(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # ["1678928760000","24341.4","24344","24313.2","24323","628","2.5819","62800","0"], + # ["1678928700000","24324.1","24347.6","24321.7","24341.4","2565","10.5401","256500","1"], + # ["1678928640000","24300.2","24324.1","24288","24324.1","3304","13.5937","330400","1"], + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ohlcvs(data, market, timeframe, since, limit) + + async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical funding rate prices + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history + + :param str symbol: unified symbol of the market to fetch the funding rate history for + :param int [since]: timestamp in ms of the earliest funding rate to fetch + :param int [limit]: the maximum amount of `funding rate structures ` to fetch + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `funding rate structures ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument') + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate') + if paginate: + return await self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params, 100) + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit + response = await self.publicGetPublicFundingRateHistory(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # }, + # { + # "instType":"SWAP", + # "instId":"BTC-USDT-SWAP", + # "fundingRate":"0.018", + # "realizedRate":"0.017", + # "fundingTime":"1597026383085" + # } + # ] + # } + # + rates = [] + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + rate = data[i] + timestamp = self.safe_integer(rate, 'fundingTime') + rates.append({ + 'info': rate, + 'symbol': self.safe_symbol(self.safe_string(rate, 'instId')), + 'fundingRate': self.safe_number(rate, 'realizedRate'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + }) + sorted = self.sort_by(rates, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_balance_by_type(self, type, response): + if type == 'funding': + return self.parse_funding_balance(response) + else: + return self.parse_trading_balance(response) + + def parse_trading_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + timestamp = self.safe_integer(first, 'uTime') + details = self.safe_list(first, 'details', []) + for i in range(0, len(details)): + balance = details[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + eq = self.safe_string(balance, 'eq') + availEq = self.safe_string(balance, 'availEq') + if (eq is None) or (availEq is None): + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + else: + account['total'] = eq + account['free'] = availEq + result[code] = account + result['timestamp'] = timestamp + result['datetime'] = self.iso8601(timestamp) + return self.safe_balance(result) + + def parse_funding_balance(self, response): + result: dict = {'info': response} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + balance = data[i] + currencyId = self.safe_string(balance, 'ccy') + code = self.safe_currency_code(currencyId) + account = self.account() + # it may be incorrect to use total, free and used for swap accounts + account['total'] = self.safe_string(balance, 'bal') + account['free'] = self.safe_string(balance, 'availBal') + account['used'] = self.safe_string(balance, 'frozenBal') + result[code] = account + return self.safe_balance(result) + + def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface: + # https://www.okx.com/docs-v5/en/#rest-api-account-get-fee-rates + # + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # + return { + 'info': fee, + 'symbol': self.safe_symbol(None, market), + # OKX returns the fees values opposed to other exchanges, so the sign needs to be flipped + 'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))), + 'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))), + 'percentage': None, + 'tierBased': None, + } + + async def fetch_trading_fee(self, symbol: str, params={}) -> TradingFeeInterface: + """ + fetch the trading fees for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-fee-rates + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `fee structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instType': self.convert_to_instrument_type(market['type']), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # "instId": market["id"], # only applicable to SPOT/MARGIN + # "uly": market["id"], # only applicable to FUTURES/SWAP/OPTION + # "category": "1", # 1 = Class A, 2 = Class B, 3 = Class C, 4 = Class D + } + if market['spot']: + request['instId'] = market['id'] + elif market['swap'] or market['future'] or market['option']: + request['uly'] = market['baseId'] + '-' + market['quoteId'] + else: + raise NotSupported(self.id + ' fetchTradingFee() supports spot, swap, future or option markets only') + response = await self.privateGetAccountTradeFee(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "category": "1", + # "delivery": "", + # "exercise": "", + # "instType": "SPOT", + # "level": "Lv1", + # "maker": "-0.0008", + # "taker": "-0.001", + # "ts": "1639043138472" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + return self.parse_trading_fee(first, market) + + async def fetch_balance(self, params={}) -> Balances: + """ + query for balance and get the amount of funds available for trading or funds locked in orders + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-balance + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance + + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.type]: wallet type, ['funding' or 'trading'] default is 'trading' + :returns dict: a `balance structure ` + """ + await self.load_markets() + marketType, query = self.handle_market_type_and_params('fetchBalance', None, params) + request: dict = { + # 'ccy': 'BTC,ETH', # comma-separated list of currency ids + } + response = None + if marketType == 'funding': + response = await self.privateGetAssetBalances(self.extend(request, query)) + else: + response = await self.privateGetAccountBalance(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "", + # "availEq": "28.21006347", + # "cashBal": "28.21006347", + # "ccy": "USDT", + # "crossLiab": "", + # "disEq": "28.2687404020176", + # "eq":"28 .21006347", + # "eqUsd": "28.2687404020176", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "0", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "0", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621556539861", + # "upl": "0", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "0", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "28.2687404020176", + # "uTime": "1621556553510" + # } + # ], + # "msg": "" + # } + # + # { + # "code": "0", + # "data": [ + # { + # "adjEq": "", + # "details": [ + # { + # "availBal": "0.049", + # "availEq": "", + # "cashBal": "0.049", + # "ccy": "BTC", + # "crossLiab": "", + # "disEq": "1918.55678", + # "eq": "0.049", + # "eqUsd": "1918.55678", + # "frozenBal": "0", + # "interest": "", + # "isoEq": "", + # "isoLiab": "", + # "liab": "", + # "maxLoan": "", + # "mgnRatio": "", + # "notionalLever": "", + # "ordFrozen": "0", + # "twap": "0", + # "uTime": "1621973128591", + # "upl": "", + # "uplLiab": "" + # } + # ], + # "imr": "", + # "isoEq": "", + # "mgnRatio": "", + # "mmr": "", + # "notionalUsd": "", + # "ordFroz": "", + # "totalEq": "1918.55678", + # "uTime": "1622045126908" + # } + # ], + # "msg": "" + # } + # + # funding + # + # { + # "code": "0", + # "data": [ + # { + # "availBal": "0.00005426", + # "bal": 0.0000542600000000, + # "ccy": "BTC", + # "frozenBal": "0" + # } + # ], + # "msg": "" + # } + # + return self.parse_balance_by_type(marketType, response) + + async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}): + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + create a market buy order by providing the symbol and cost + :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 ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketBuyOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return await self.create_order(symbol, 'market', 'buy', cost, None, self.extend(req, params)) + + async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}): + """ + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + + create a market buy order by providing the symbol and cost + :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 ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['spot']: + raise NotSupported(self.id + ' createMarketSellOrderWithCost() supports spot markets only') + req = { + 'createMarketBuyOrderRequiresPrice': False, + 'tgtCcy': 'quote_ccy', + } + return await self.create_order(symbol, 'market', 'sell', cost, None, self.extend(req, params)) + + def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ccy': currency['id'], # only applicable to cross MARGIN orders in single-currency margin + # 'clOrdId': clientOrderId, # up to 32 characters, must be unique + # 'tag': tag, # up to 8 characters + 'side': side, + # 'posSide': 'long', # long, short, # required in the long/short mode, and can only be long or short(only for future or swap) + 'ordType': type, + # 'ordType': type, # privatePostTradeOrder: market, limit, post_only, fok, ioc, optimal_limit_ioc + # 'ordType': type, # privatePostTradeOrderAlgo: conditional, oco, trigger, move_order_stop, iceberg, twap + 'sz': self.amount_to_precision(symbol, amount), + # 'px': self.price_to_precision(symbol, price), # limit orders only + # 'reduceOnly': False, + # + # 'triggerPx': 10, # stopPrice(trigger orders) + # 'orderPx': 10, # Order price if -1, the order will be executed at the market price.(trigger orders) + # 'triggerPxType': 'last', # Conditional default is last, mark or index(trigger orders) + # + # 'tpTriggerPx': 10, # takeProfitPrice(conditional orders) + # 'tpTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'tpOrdPx': 10, # Order price for Take-Profit orders, if -1 will be executed at market price(conditional orders) + # + # 'slTriggerPx': 10, # stopLossPrice(conditional orders) + # 'slTriggerPxType': 'last', # Conditional default is last, mark or index(conditional orders) + # 'slOrdPx': 10, # Order price for Stop-Loss orders, if -1 will be executed at market price(conditional orders) + } + spot = market['spot'] + contract = market['contract'] + triggerPrice = self.safe_value_n(params, ['triggerPrice', 'stopPrice', 'triggerPx']) + timeInForce = self.safe_string(params, 'timeInForce', 'GTC') + takeProfitPrice = self.safe_value_2(params, 'takeProfitPrice', 'tpTriggerPx') + tpOrdPx = self.safe_value(params, 'tpOrdPx', price) + tpTriggerPxType = self.safe_string(params, 'tpTriggerPxType', 'last') + stopLossPrice = self.safe_value_2(params, 'stopLossPrice', 'slTriggerPx') + slOrdPx = self.safe_value(params, 'slOrdPx', price) + slTriggerPxType = self.safe_string(params, 'slTriggerPxType', 'last') + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + stopLoss = self.safe_value(params, 'stopLoss') + stopLossDefined = (stopLoss is not None) + takeProfit = self.safe_value(params, 'takeProfit') + takeProfitDefined = (takeProfit is not None) + trailingPercent = self.safe_string_2(params, 'trailingPercent', 'callbackRatio') + isTrailingPercentOrder = trailingPercent is not None + trigger = (triggerPrice is not None) or (type == 'trigger') + isReduceOnly = self.safe_value(params, 'reduceOnly', False) + defaultMarginMode = self.safe_string_2(self.options, 'defaultMarginMode', 'marginMode', 'cross') + marginMode = self.safe_string_2(params, 'marginMode', 'tdMode') # cross or isolated, tdMode not ommited so be extended into the request + margin = False + if (marginMode is not None) and (marginMode != 'cash'): + margin = True + else: + marginMode = defaultMarginMode + margin = self.safe_bool(params, 'margin', False) + if spot: + if margin: + defaultCurrency = market['quote'] if (side == 'buy') else market['base'] + currency = self.safe_string(params, 'ccy', defaultCurrency) + request['ccy'] = self.safe_currency_code(currency) + tradeMode = marginMode if margin else 'cash' + request['tdMode'] = tradeMode + elif contract: + if market['swap'] or market['future']: + positionSide = None + positionSide, params = self.handle_option_and_params(params, 'createOrder', 'positionSide') + if positionSide is not None: + request['posSide'] = positionSide + else: + hedged = None + hedged, params = self.handle_option_and_params(params, 'createOrder', 'hedged') + if hedged: + isBuy = (side == 'buy') + isProtective = (takeProfitPrice is not None) or (stopLossPrice is not None) or isReduceOnly + if isProtective: + # in case of protective orders, the posSide should be opposite of position side + # reduceOnly is emulated and not natively supported by the exchange + request['posSide'] = 'short' if isBuy else 'long' + if isReduceOnly: + params = self.omit(params, 'reduceOnly') + else: + request['posSide'] = 'long' if isBuy else 'short' + request['tdMode'] = marginMode + isMarketOrder = type == 'market' + postOnly = False + postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params) + params = self.omit(params, ['currency', 'ccy', 'marginMode', 'timeInForce', 'stopPrice', 'triggerPrice', 'clientOrderId', 'stopLossPrice', 'takeProfitPrice', 'slOrdPx', 'tpOrdPx', 'margin', 'stopLoss', 'takeProfit', 'trailingPercent']) + ioc = (timeInForce == 'IOC') or (type == 'ioc') + fok = (timeInForce == 'FOK') or (type == 'fok') + conditional = (stopLossPrice is not None) or (takeProfitPrice is not None) or (type == 'conditional') + marketIOC = (isMarketOrder and ioc) or (type == 'optimal_limit_ioc') + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(params, 'tgtCcy', defaultTgtCcy) + if (not contract) and (not margin): + request['tgtCcy'] = tgtCcy + if isMarketOrder or marketIOC: + request['ordType'] = 'market' + if spot and (side == 'buy'): + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + if tgtCcy == 'quote_ccy': + # quote_ccy: sz refers to units of quote currency + createMarketBuyOrderRequiresPrice = True + createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True) + notional = self.safe_number_2(params, 'cost', 'sz') + params = self.omit(params, ['cost', 'sz']) + if createMarketBuyOrderRequiresPrice: + if price is not None: + if notional is None: + amountString = self.number_to_string(amount) + priceString = self.number_to_string(price) + quoteAmount = Precise.string_mul(amountString, priceString) + notional = self.parse_number(quoteAmount) + elif notional is None: + raise InvalidOrder(self.id + " createOrder() requires the price argument with market buy orders to calculate total order cost(amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options['createMarketBuyOrderRequiresPrice'] = False and supply the total cost value in the 'amount' argument or in the 'cost' unified extra parameter or in exchange-specific 'sz' extra parameter(the exchange-specific behaviour)") + else: + notional = amount if (notional is None) else notional + request['sz'] = self.cost_to_precision(symbol, notional) + if marketIOC and contract: + request['ordType'] = 'optimal_limit_ioc' + else: + if (not trigger) and (not conditional): + request['px'] = self.price_to_precision(symbol, price) + if postOnly: + request['ordType'] = 'post_only' + elif ioc and not marketIOC: + request['ordType'] = 'ioc' + elif fok: + request['ordType'] = 'fok' + if isTrailingPercentOrder: + convertedTrailingPercent = Precise.string_div(trailingPercent, '100') + request['callbackRatio'] = convertedTrailingPercent + request['ordType'] = 'move_order_stop' + elif stopLossDefined or takeProfitDefined: + if stopLossDefined: + stopLossTriggerPrice = self.safe_value_n(stopLoss, ['triggerPrice', 'stopPrice', 'slTriggerPx']) + if stopLossTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["stopLoss"]["triggerPrice"], or params["stopLoss"]["stopPrice"], or params["stopLoss"]["slTriggerPx"] for a stop loss order') + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + stopLossLimitPrice = self.safe_value_n(stopLoss, ['price', 'stopLossPrice', 'slOrdPx']) + stopLossOrderType = self.safe_string(stopLoss, 'type') + if stopLossOrderType is not None: + stopLossLimitOrderType = (stopLossOrderType == 'limit') + stopLossMarketOrderType = (stopLossOrderType == 'market') + if (not stopLossLimitOrderType) and (not stopLossMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["stopLoss"]["type"] must be either "limit" or "market"') + elif stopLossLimitOrderType: + if stopLossLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["stopLoss"]["price"] or params["stopLoss"]["slOrdPx"] for a stop loss limit order') + else: + request['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) + elif stopLossOrderType == 'market': + request['slOrdPx'] = '-1' + elif stopLossLimitPrice is not None: + request['slOrdPx'] = self.price_to_precision(symbol, stopLossLimitPrice) # limit sl order + else: + request['slOrdPx'] = '-1' # market sl order + stopLossTriggerPriceType = self.safe_string_2(stopLoss, 'triggerPriceType', 'slTriggerPxType', 'last') + if stopLossTriggerPriceType is not None: + if (stopLossTriggerPriceType != 'last') and (stopLossTriggerPriceType != 'index') and (stopLossTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() stop loss trigger price type must be one of "last", "index" or "mark"') + request['slTriggerPxType'] = stopLossTriggerPriceType + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value_n(takeProfit, ['triggerPrice', 'stopPrice', 'tpTriggerPx']) + if takeProfitTriggerPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a trigger price in params["takeProfit"]["triggerPrice"], or params["takeProfit"]["stopPrice"], or params["takeProfit"]["tpTriggerPx"] for a take profit order') + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + takeProfitLimitPrice = self.safe_value_n(takeProfit, ['price', 'takeProfitPrice', 'tpOrdPx']) + takeProfitOrderType = self.safe_string_2(takeProfit, 'type', 'tpOrdKind') + if takeProfitOrderType is not None: + takeProfitLimitOrderType = (takeProfitOrderType == 'limit') + takeProfitMarketOrderType = (takeProfitOrderType == 'market') + if (not takeProfitLimitOrderType) and (not takeProfitMarketOrderType): + raise InvalidOrder(self.id + ' createOrder() params["takeProfit"]["type"] must be either "limit" or "market"') + elif takeProfitLimitOrderType: + if takeProfitLimitPrice is None: + raise InvalidOrder(self.id + ' createOrder() requires a limit price in params["takeProfit"]["price"] or params["takeProfit"]["tpOrdPx"] for a take profit limit order') + else: + request['tpOrdKind'] = takeProfitOrderType + request['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) + elif takeProfitOrderType == 'market': + request['tpOrdPx'] = '-1' + elif takeProfitLimitPrice is not None: + request['tpOrdKind'] = 'limit' + request['tpOrdPx'] = self.price_to_precision(symbol, takeProfitLimitPrice) # limit tp order + else: + request['tpOrdPx'] = '-1' # market tp order + takeProfitTriggerPriceType = self.safe_string_2(takeProfit, 'triggerPriceType', 'tpTriggerPxType', 'last') + if takeProfitTriggerPriceType is not None: + if (takeProfitTriggerPriceType != 'last') and (takeProfitTriggerPriceType != 'index') and (takeProfitTriggerPriceType != 'mark'): + raise InvalidOrder(self.id + ' createOrder() take profit trigger price type must be one of "last", "index" or "mark"') + request['tpTriggerPxType'] = takeProfitTriggerPriceType + elif trigger: + request['ordType'] = 'trigger' + request['triggerPx'] = self.price_to_precision(symbol, triggerPrice) + request['orderPx'] = '-1' if isMarketOrder else self.price_to_precision(symbol, price) + elif conditional: + request['ordType'] = 'conditional' + twoWayCondition = ((takeProfitPrice is not None) and (stopLossPrice is not None)) + # if TP and SL are sent together + # 'conditional' only stop-loss order will be applied + # tpOrdKind is 'condition' which is the default + if twoWayCondition: + request['ordType'] = 'oco' + if takeProfitPrice is not None: + request['tpTriggerPx'] = self.price_to_precision(symbol, takeProfitPrice) + tpOrdPxReq = '-1' + if tpOrdPx is not None: + tpOrdPxReq = self.price_to_precision(symbol, tpOrdPx) + request['tpOrdPx'] = tpOrdPxReq + request['tpTriggerPxType'] = tpTriggerPxType + if stopLossPrice is not None: + request['slTriggerPx'] = self.price_to_precision(symbol, stopLossPrice) + slOrdPxReq = '-1' + if slOrdPx is not None: + slOrdPxReq = self.price_to_precision(symbol, slOrdPx) + request['slOrdPx'] = slOrdPxReq + request['slTriggerPxType'] = slTriggerPxType + if clientOrderId is None: + brokerId = self.safe_string(self.options, 'brokerId') + if brokerId is not None: + request['clOrdId'] = brokerId + self.uuid16() + request['tag'] = brokerId + else: + request['clOrdId'] = clientOrderId + params = self.omit(params, ['clOrdId', 'clientOrderId']) + return self.extend(request, params) + + async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): + """ + create a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-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.reduceOnly]: a mark to reduce the position size for margin, swap and future orders + :param bool [params.postOnly]: True to place a post only order + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered(perpetual swap markets only) + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered(perpetual swap markets only) + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param str [params.positionSide]: if position mode is one-way: set to 'net', if position mode is hedge-mode: set to 'long' or 'short' + :param str [params.trailingPercent]: the percent to trail away from the current market price + :param str [params.tpOrdKind]: 'condition' or 'limit', the default is 'condition' + :param bool [params.hedged]: *swap and future only* True for hedged mode, False for one way mode + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.create_order_request(symbol, type, side, amount, price, params) + method = self.safe_string(self.options, 'createOrder', 'privatePostTradeBatchOrders') + requestOrdType = self.safe_string(request, 'ordType') + if (requestOrdType == 'trigger') or (requestOrdType == 'conditional') or (requestOrdType == 'move_order_stop') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + method = 'privatePostTradeOrderAlgo' + if (method != 'privatePostTradeOrder') and (method != 'privatePostTradeOrderAlgo') and (method != 'privatePostTradeBatchOrders'): + raise ExchangeError(self.id + ' createOrder() self.options["createOrder"] must be either privatePostTradeBatchOrders or privatePostTradeOrder or privatePostTradeOrderAlgo') + if method == 'privatePostTradeBatchOrders': + # keep the request body the same + # submit a single order in an array to the batch order endpoint + # because it has a lower ratelimit + request = [request] + response = None + if method == 'privatePostTradeOrder': + response = await self.privatePostTradeOrder(request) + elif method == 'privatePostTradeOrderAlgo': + response = await self.privatePostTradeOrderAlgo(request) + else: + response = await self.privatePostTradeBatchOrders(request) + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + async def create_orders(self, orders: List[OrderRequest], params={}): + """ + create a list of trade orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-multiple-orders + + :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `order structure ` + """ + await self.load_markets() + ordersRequests = [] + for i in range(0, len(orders)): + rawOrder = orders[i] + marketId = self.safe_string(rawOrder, 'symbol') + type = self.safe_string(rawOrder, 'type') + side = self.safe_string(rawOrder, 'side') + amount = self.safe_value(rawOrder, 'amount') + price = self.safe_value(rawOrder, 'price') + orderParams = self.safe_dict(rawOrder, 'params', {}) + extendedParams = self.extend(orderParams, params) # the request does not accept extra params since it's a list, so we're extending each order with the common params + orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams) + ordersRequests.append(orderRequest) + response = await self.privatePostTradeBatchOrders(ordersRequests) + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc7f2a1b4c4509f82", + # "ordId": "636305438765568000", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # }, + # { + # "clOrdId": "e847386590ce4dBC0b9993fe642d8f62", + # "ordId": "636305438765568001", + # "sCode": "0", + # "sMsg": "Order placed", + # "tag": "e847386590ce4dBC" + # } + # ], + # "inTime": "1697979038584486", + # "msg": "", + # "outTime": "1697979038586493" + # } + data = self.safe_list(response, 'data', []) + return self.parse_orders(data) + + def edit_order_request(self, id: str, symbol, type, side, amount=None, price=None, params={}): + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + if isAlgoOrder: + request['algoClOrdId'] = clientOrderId + else: + request['clOrdId'] = clientOrderId + else: + if isAlgoOrder: + request['algoId'] = id + else: + request['ordId'] = id + stopLossTriggerPrice = self.safe_value_2(params, 'stopLossPrice', 'newSlTriggerPx') + stopLossPrice = self.safe_value(params, 'newSlOrdPx') + stopLossTriggerPriceType = self.safe_string(params, 'newSlTriggerPxType', 'last') + takeProfitTriggerPrice = self.safe_value_2(params, 'takeProfitPrice', 'newTpTriggerPx') + takeProfitPrice = self.safe_value(params, 'newTpOrdPx') + takeProfitTriggerPriceType = self.safe_string(params, 'newTpTriggerPxType', 'last') + stopLoss = self.safe_value(params, 'stopLoss') + takeProfit = self.safe_value(params, 'takeProfit') + stopLossDefined = (stopLoss is not None) + takeProfitDefined = (takeProfit is not None) + if isAlgoOrder: + if (stopLossTriggerPrice is None) and (takeProfitTriggerPrice is None): + raise BadRequest(self.id + ' editOrder() requires a stopLossPrice or takeProfitPrice parameter for editing an algo order') + if stopLossTriggerPrice is not None: + if stopLossPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newSlOrdPx parameter for editing an algo order') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + if takeProfitPrice is None: + raise BadRequest(self.id + ' editOrder() requires a newTpOrdPx parameter for editing an algo order') + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + else: + if stopLossTriggerPrice is not None: + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitTriggerPrice is not None: + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (type == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if stopLossDefined: + stopLossTriggerPrice = self.safe_value(stopLoss, 'triggerPrice') + stopLossPrice = self.safe_value(stopLoss, 'price') + stopLossType = self.safe_string(stopLoss, 'type') + request['newSlTriggerPx'] = self.price_to_precision(symbol, stopLossTriggerPrice) + request['newSlOrdPx'] = '-1' if (stopLossType == 'market') else self.price_to_precision(symbol, stopLossPrice) + request['newSlTriggerPxType'] = stopLossTriggerPriceType + if takeProfitDefined: + takeProfitTriggerPrice = self.safe_value(takeProfit, 'triggerPrice') + takeProfitPrice = self.safe_value(takeProfit, 'price') + takeProfitType = self.safe_string(takeProfit, 'type') + request['newTpOrdKind'] = takeProfitType if (takeProfitType == 'limit') else 'condition' + request['newTpTriggerPx'] = self.price_to_precision(symbol, takeProfitTriggerPrice) + request['newTpOrdPx'] = '-1' if (takeProfitType == 'market') else self.price_to_precision(symbol, takeProfitPrice) + request['newTpTriggerPxType'] = takeProfitTriggerPriceType + if amount is not None: + request['newSz'] = self.amount_to_precision(symbol, amount) + if not isAlgoOrder: + if price is not None: + request['newPx'] = self.price_to_precision(symbol, price) + params = self.omit(params, ['clOrdId', 'clientOrderId', 'takeProfitPrice', 'stopLossPrice', 'stopLoss', 'takeProfit', 'postOnly']) + return self.extend(request, params) + + async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}): + """ + edit a trade order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-amend-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-amend-algo-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 str [params.clientOrderId]: client order id, uses id if not passed + :param float [params.stopLossPrice]: stop loss trigger price + :param float [params.newSlOrdPx]: the stop loss order price, set to stopLossPrice if the type is market + :param str [params.newSlTriggerPxType]: 'last', 'index' or 'mark' used to specify the stop loss trigger price type, default is 'last' + :param float [params.takeProfitPrice]: take profit trigger price + :param float [params.newTpOrdPx]: the take profit order price, set to takeProfitPrice if the type is market + :param str [params.newTpTriggerPxType]: 'last', 'index' or 'mark' used to specify the take profit trigger price type, default is 'last' + :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered + :param float [params.stopLoss.triggerPrice]: stop loss trigger price + :param float [params.stopLoss.price]: used for stop loss limit orders, not used for stop loss market price orders + :param str [params.stopLoss.type]: 'market' or 'limit' used to specify the stop loss price type + :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered + :param float [params.takeProfit.triggerPrice]: take profit trigger price + :param float [params.takeProfit.price]: used for take profit limit orders, not used for take profit market price orders + :param str [params.takeProfit.type]: 'market' or 'limit' used to specify the take profit price type + :param str [params.newTpOrdKind]: 'condition' or 'limit', the default is 'condition' + :returns dict: an `order structure ` + """ + await self.load_markets() + market = self.market(symbol) + request = self.edit_order_request(id, symbol, type, side, amount, price, params) + isAlgoOrder = None + if (type == 'trigger') or (type == 'conditional') or (type == 'move_order_stop') or (type == 'oco') or (type == 'iceberg') or (type == 'twap'): + isAlgoOrder = True + response = None + if isAlgoOrder: + response = await self.privatePostTradeAmendAlgos(self.extend(request, params)) + else: + response = await self.privatePostTradeAmendOrder(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + first = self.safe_dict(data, 0, {}) + order = self.parse_order(first, market) + order['type'] = type + order['side'] = side + return order + + async def cancel_order(self, id: str, symbol: Str = None, params={}): + """ + cancels an open order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-order + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str id: order id + :param str symbol: unified symbol of the market the order was made in + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: True if trigger orders + :param boolean [params.trailing]: set to True if you want to cancel a trailing order + :returns dict: An `order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + orderInner = await self.cancel_orders([id], symbol, params) + return self.safe_value(orderInner, 0) + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'ordId': id, # either ordId or clOrdId is required + # 'clOrdId': clientOrderId, + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['clOrdId', 'clientOrderId']) + response = await self.privatePostTradeCancelOrder(self.extend(request, query)) + # {"code":"0","data":[{"clOrdId":"","ordId":"317251910906576896","sCode":"0","sMsg":""}],"msg":""} + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + def parse_ids(self, ids): + """ + @ignore + :param string[]|str ids: order ids + :returns str[]: list of order ids + """ + if (ids is not None) and isinstance(ids, str): + return ids.split(',') + else: + return ids + + async def cancel_orders(self, ids, symbol: Str = None, params={}): + """ + cancel multiple orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param str[] ids: order ids + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too. + if symbol is None: + raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request = [] + options = self.safe_value(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + clientOrderIds = self.parse_ids(self.safe_value_2(params, 'clOrdId', 'clientOrderId')) + algoIds = self.parse_ids(self.safe_value(params, 'algoId')) + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trigger or trailing: + method = 'privatePostTradeCancelAlgos' + if clientOrderIds is None: + ids = self.parse_ids(ids) + if algoIds is not None: + for i in range(0, len(algoIds)): + request.append({ + 'algoId': algoIds[i], + 'instId': market['id'], + }) + for i in range(0, len(ids)): + if trailing or trigger: + request.append({ + 'algoId': ids[i], + 'instId': market['id'], + }) + else: + request.append({ + 'ordId': ids[i], + 'instId': market['id'], + }) + else: + for i in range(0, len(clientOrderIds)): + request.append({ + 'instId': market['id'], + 'clOrdId': clientOrderIds[i], + }) + response = None + if method == 'privatePostTradeCancelAlgos': + response = await self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = await self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, market, None, None, params) + + async def cancel_orders_for_symbols(self, orders: List[CancellationRequest], params={}): + """ + cancel multiple orders for multiple symbols + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-multiple-orders + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-cancel-algo-order + + :param CancellationRequest[] orders: each order should contain the parameters required by cancelOrder namely id and symbol, example [{"id": "a", "symbol": "BTC/USDT"}, {"id": "b", "symbol": "ETH/USDT"}] + :param dict [params]: extra parameters specific to the exchange API endpoint + :param boolean [params.trigger]: whether the order is a stop/trigger order + :param boolean [params.trailing]: set to True if you want to cancel trailing orders + :returns dict: an list of `order structures ` + """ + await self.load_markets() + request = [] + options = self.safe_dict(self.options, 'cancelOrders', {}) + defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + isStopOrTrailing = trigger or trailing + if isStopOrTrailing: + method = 'privatePostTradeCancelAlgos' + for i in range(0, len(orders)): + order = orders[i] + id = self.safe_string(order, 'id') + clientOrderId = self.safe_string_2(order, 'clOrdId', 'clientOrderId') + symbol = self.safe_string(order, 'symbol') + market = self.market(symbol) + idKey = 'ordId' + if isStopOrTrailing: + idKey = 'algoId' + elif clientOrderId is not None: + idKey = 'clOrdId' + requestItem: dict = { + 'instId': market['id'], + } + requestItem[idKey] = clientOrderId if (clientOrderId is not None) else id + request.append(requestItem) + response = None + if method == 'privatePostTradeCancelAlgos': + response = await self.privatePostTradeCancelAlgos(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + else: + response = await self.privatePostTradeCancelBatchOrders(request) # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT + # + # { + # "code": "0", + # "data": [ + # { + # "clOrdId": "e123456789ec4dBC1123456ba123b45e", + # "ordId": "405071912345641543", + # "sCode": "0", + # "sMsg": "" + # }, + # ... + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "algoId": "431375349042380800", + # "sCode": "0", + # "sMsg": "" + # } + # ], + # "msg": "" + # } + # + ordersData = self.safe_list(response, 'data', []) + return self.parse_orders(ordersData, None, None, None, params) + + async def cancel_all_orders_after(self, timeout: Int, params={}): + """ + dead man's switch, cancel all orders after the given timeout + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-all-after + + :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 + """ + await self.load_markets() + request: dict = { + 'timeOut': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0, + } + response = await self.privatePostTradeCancelAllAfter(self.extend(request, params)) + # + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "triggerTime":"1587971460", + # "ts":"1587971400" + # } + # ] + # } + # + return response + + def parse_order_status(self, status: Str): + statuses: dict = { + 'canceled': 'canceled', + 'order_failed': 'canceled', + 'live': 'open', + 'partially_filled': 'open', + 'filled': 'closed', + 'effective': 'closed', + } + return self.safe_string(statuses, status, status) + + def parse_order(self, order: dict, market: Market = None) -> Order: + # + # createOrder + # + # { + # "clOrdId": "oktswap6", + # "ordId": "312269865356374016", + # "tag": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # editOrder + # + # { + # "clOrdId": "e847386590ce4dBCc1a045253497a547", + # "ordId": "559176536793178112", + # "reqId": "", + # "sCode": "0", + # "sMsg": "" + # } + # + # Spot and Swap fetchOrder, fetchOpenOrders + # + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # + # Algo Order fetchOpenOrders, fetchCanceledOrders, fetchClosedOrders + # + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # + scode = self.safe_string(order, 'sCode') + if (scode is not None) and (scode != '0'): + return self.safe_order({ + 'id': self.safe_string(order, 'ordId'), + 'clientOrderId': self.safe_string(order, 'clOrdId'), + 'status': 'rejected', + 'info': order, + }) + id = self.safe_string_2(order, 'algoId', 'ordId') + timestamp = self.safe_integer(order, 'cTime') + lastUpdateTimestamp = self.safe_integer(order, 'uTime') + lastTradeTimestamp = self.safe_integer(order, 'fillTime') + side = self.safe_string(order, 'side') + type = self.safe_string(order, 'ordType') + postOnly = None + timeInForce = None + if type == 'post_only': + postOnly = True + type = 'limit' + elif type == 'fok': + timeInForce = 'FOK' + type = 'limit' + elif type == 'ioc': + timeInForce = 'IOC' + type = 'limit' + marketId = self.safe_string(order, 'instId') + market = self.safe_market(marketId, market) + symbol = self.safe_symbol(marketId, market, '-') + filled = self.safe_string(order, 'accFillSz') + price = self.safe_string_2(order, 'px', 'ordPx') + average = self.safe_string(order, 'avgPx') + status = self.parse_order_status(self.safe_string(order, 'state')) + feeCostString = self.safe_string(order, 'fee') + amount = None + cost = None + # spot market buy: "sz" can refer either to base currency units or to quote currency units + # see documentation: https://www.okx.com/docs-v5/en/#rest-api-trade-place-order + defaultTgtCcy = self.safe_string(self.options, 'tgtCcy', 'base_ccy') + tgtCcy = self.safe_string(order, 'tgtCcy', defaultTgtCcy) + instType = self.safe_string(order, 'instType') + if (side == 'buy') and (type == 'market') and (instType == 'SPOT') and (tgtCcy == 'quote_ccy'): + # "sz" refers to the cost + cost = self.safe_string(order, 'sz') + else: + # "sz" refers to the trade currency amount + amount = self.safe_string(order, 'sz') + fee = None + if feeCostString is not None: + feeCostSigned = Precise.string_neg(feeCostString) + feeCurrencyId = self.safe_string(order, 'feeCcy') + feeCurrencyCode = self.safe_currency_code(feeCurrencyId) + fee = { + 'cost': self.parse_number(feeCostSigned), + 'currency': feeCurrencyCode, + } + clientOrderId = self.safe_string(order, 'clOrdId') + if (clientOrderId is not None) and (len(clientOrderId) < 1): + clientOrderId = None # fix empty clientOrderId string + stopLossPrice = self.safe_number_2(order, 'slTriggerPx', 'slOrdPx') + takeProfitPrice = self.safe_number_2(order, 'tpTriggerPx', 'tpOrdPx') + reduceOnlyRaw = self.safe_string(order, 'reduceOnly') + reduceOnly = False + if reduceOnly is not None: + reduceOnly = (reduceOnlyRaw == 'true') + return self.safe_order({ + 'info': order, + 'id': id, + 'clientOrderId': clientOrderId, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastTradeTimestamp': lastTradeTimestamp, + 'lastUpdateTimestamp': lastUpdateTimestamp, + 'symbol': symbol, + 'type': type, + 'timeInForce': timeInForce, + 'postOnly': postOnly, + 'side': side, + 'price': price, + 'stopLossPrice': stopLossPrice, + 'takeProfitPrice': takeProfitPrice, + 'triggerPrice': self.safe_number_n(order, ['triggerPx', 'moveTriggerPx']), + 'average': average, + 'cost': cost, + 'amount': amount, + 'filled': filled, + 'remaining': None, + 'status': status, + 'fee': fee, + 'trades': None, + 'reduceOnly': reduceOnly, + }, market) + + async def fetch_order(self, id: str, symbol: Str = None, params={}): + """ + fetch an order by the id + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-details + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-details + + :param str id: the order id + :param str symbol: unified market symbol + :param dict [params]: extra and exchange specific parameters + :param boolean [params.trigger]: True if fetching trigger orders + :returns: `an order structure ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + # 'clOrdId': 'abcdef12345', # optional, [a-z0-9]{1,32} + # 'ordId': id, + # 'instType': # spot, swap, futures, margin + } + clientOrderId = self.safe_string_2(params, 'clOrdId', 'clientOrderId') + options = self.safe_value(self.options, 'fetchOrder', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrder') + method = self.safe_string(params, 'method', defaultMethod) + trigger = self.safe_value_2(params, 'stop', 'trigger') + if trigger: + method = 'privateGetTradeOrderAlgo' + if clientOrderId is not None: + request['algoClOrdId'] = clientOrderId + else: + request['algoId'] = id + else: + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + else: + request['ordId'] = id + query = self.omit(params, ['method', 'clOrdId', 'clientOrderId', 'stop', 'trigger']) + response = None + if method == 'privateGetTradeOrderAlgo': + response = await self.privateGetTradeOrderAlgo(self.extend(request, query)) + else: + response = await self.privateGetTradeOrder(self.extend(request, query)) + # + # Spot and Swap + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # { + # "code":"0", + # "msg":"", + # "data":[ + # { + # "instType":"FUTURES", + # "instId":"BTC-USD-200329", + # "ordId":"123445", + # "ccy":"BTC", + # "clOrdId":"", + # "algoId":"1234", + # "sz":"999", + # "closeFraction":"", + # "ordType":"oco", + # "side":"buy", + # "posSide":"long", + # "tdMode":"cross", + # "tgtCcy": "", + # "state":"effective", + # "lever":"20", + # "tpTriggerPx":"", + # "tpTriggerPxType":"", + # "tpOrdPx":"", + # "slTriggerPx":"", + # "slTriggerPxType":"", + # "triggerPx":"99", + # "triggerPxType":"last", + # "ordPx":"12", + # "actualSz":"", + # "actualPx":"", + # "actualSide":"", + # "pxVar":"", + # "pxSpread":"", + # "pxLimit":"", + # "szLimit":"", + # "tag": "adadadadad", + # "timeInterval":"", + # "callbackRatio":"", + # "callbackSpread":"", + # "activePx":"", + # "moveTriggerPx":"", + # "reduceOnly": "false", + # "triggerTime":"1597026383085", + # "last": "16012", + # "failCode": "", + # "algoClOrdId": "", + # "cTime":"1597026383000" + # } + # ] + # } + # + data = self.safe_value(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetch all unfilled currently open orders + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-list + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-list + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch open orders for + :param int [limit]: the maximum number of open orders structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated, stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'live', # live, partially_filled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_value(self.options, 'fetchOpenOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersPending') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoPending' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger and (ordType is None): + request['ordType'] = 'trigger' + query = self.omit(params, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoPending': + response = await self.privateGetTradeOrdersAlgoPending(self.extend(request, query)) + else: + response = await self.privateGetTradeOrdersPending(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px":"20 00", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz":"0. 001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg":"" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "431375349042380800", + # "cTime": "1649119897778", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "46538.9", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "467.059", + # "ordId": "", + # "ordPx": "50000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "live", + # "sz": "1", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "50000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches information on multiple canceled orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + + :param str symbol: unified market symbol of the market orders were made in + :param int [since]: timestamp in ms of the earliest order, default is None + :param int [limit]: max number of orders to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns dict: a list of `order structures ` + """ + await self.load_markets() + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'canceled', # filled, canceled + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchCanceledOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request['state'] = 'canceled' + options = self.safe_value(self.options, 'fetchCanceledOrders', {}) + algoOrderTypes = self.safe_value(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_value_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing: + method = 'privateGetTradeOrdersAlgoHistory' + request['ordType'] = 'move_order_stop' + elif trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + algoId = self.safe_string(params, 'algoId') + if algoId is not None: + request['algoId'] = algoId + params = self.omit(params, 'algoId') + if trigger: + if ordType is None: + raise ArgumentsRequired(self.id + ' fetchCanceledOrders() requires an "ordType" string parameter, "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap"') + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = await self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + else: + response = await self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1644037822494", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "BTC", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "BTC-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "410059580352409602", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "30000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "source": "", + # "state": "canceled", + # "sz": "0.0005452", + # "tag": "", + # "tdMode": "cash", + # "tgtCcy": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "tradeId": "", + # "uTime": "1644038165667" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "canceled", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: + """ + fetches information on multiple closed orders made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-7-days + https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-get-algo-order-history + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-order-history-last-3-months + + :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 bool [params.trigger]: True if fetching trigger or conditional orders + :param str [params.ordType]: "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap" + :param str [params.algoId]: Algo ID "'433845797218942976'" + :param int [params.until]: timestamp in ms to fetch orders for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :param str [params.method]: method to be used, either 'privateGetTradeOrdersHistory', 'privateGetTradeOrdersHistoryArchive' or 'privateGetTradeOrdersAlgoHistory' default is 'privateGetTradeOrdersHistory' + :param boolean [params.trailing]: set to True if you want to fetch trailing orders + :returns Order[]: a list of `order structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params) + request: dict = { + # 'instType': type.upper(), # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordType': 'limit', # market, limit, post_only, fok, ioc, comma-separated stop orders: conditional, oco, trigger, move_order_stop, iceberg, or twap + # 'state': 'filled', # filled, effective + # 'after': orderId, + # 'before': orderId, + # 'limit': limit, # default 100, max 100 + # 'algoId': "'433845797218942976'", # Algo order + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + type = None + query = None + type, query = self.handle_market_type_and_params('fetchClosedOrders', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit # default 100, max 100 + options = self.safe_dict(self.options, 'fetchClosedOrders', {}) + algoOrderTypes = self.safe_dict(self.options, 'algoOrderTypes', {}) + defaultMethod = self.safe_string(options, 'method', 'privateGetTradeOrdersHistory') + method = self.safe_string(params, 'method', defaultMethod) + ordType = self.safe_string(params, 'ordType') + trigger = self.safe_bool_2(params, 'stop', 'trigger') + trailing = self.safe_bool(params, 'trailing', False) + if trailing or trigger or (ordType in algoOrderTypes): + method = 'privateGetTradeOrdersAlgoHistory' + request['state'] = 'effective' + if trailing: + request['ordType'] = 'move_order_stop' + elif trigger: + if ordType is None: + request['ordType'] = 'trigger' + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(query, 'until') + if until is not None: + request['end'] = until + query = self.omit(query, ['until']) + request['state'] = 'filled' + send = self.omit(query, ['method', 'stop', 'trigger', 'trailing']) + response = None + if method == 'privateGetTradeOrdersAlgoHistory': + response = await self.privateGetTradeOrdersAlgoHistory(self.extend(request, send)) + elif method == 'privateGetTradeOrdersHistoryArchive': + response = await self.privateGetTradeOrdersHistoryArchive(self.extend(request, send)) + else: + response = await self.privateGetTradeOrdersHistory(self.extend(request, send)) + # + # { + # "code": "0", + # "data": [ + # { + # "accFillSz": "0", + # "avgPx": "", + # "cTime": "1621910749815", + # "category": "normal", + # "ccy": "", + # "clOrdId": "", + # "fee": "0", + # "feeCcy": "ETH", + # "fillPx": "", + # "fillSz": "0", + # "fillTime": "", + # "instId": "ETH-USDT", + # "instType": "SPOT", + # "lever": "", + # "ordId": "317251910906576896", + # "ordType": "limit", + # "pnl": "0", + # "posSide": "net", + # "px": "2000", + # "rebate": "0", + # "rebateCcy": "USDT", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "state": "live", + # "sz": "0.001", + # "tag": "", + # "tdMode": "cash", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tradeId": "", + # "uTime": "1621910749815" + # } + # ], + # "msg": "" + # } + # + # Algo order + # + # { + # "code": "0", + # "data": [ + # { + # "activePx": "", + # "activePxType": "", + # "actualPx": "", + # "actualSide": "buy", + # "actualSz": "0", + # "algoId": "433845797218942976", + # "cTime": "1649708898523", + # "callbackRatio": "", + # "callbackSpread": "", + # "ccy": "", + # "ctVal": "0.01", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "last": "39950.4", + # "lever": "125", + # "moveTriggerPx": "", + # "notionalUsd": "1592.1760000000002", + # "ordId": "", + # "ordPx": "29000", + # "ordType": "trigger", + # "posSide": "long", + # "pxLimit": "", + # "pxSpread": "", + # "pxVar": "", + # "side": "buy", + # "slOrdPx": "", + # "slTriggerPx": "", + # "slTriggerPxType": "", + # "state": "effective", + # "sz": "4", + # "szLimit": "", + # "tag": "", + # "tdMode": "isolated", + # "tgtCcy": "", + # "timeInterval": "", + # "tpOrdPx": "", + # "tpTriggerPx": "", + # "tpTriggerPxType": "", + # "triggerPx": "30000", + # "triggerPxType": "last", + # "triggerTime": "", + # "uly": "BTC-USDT" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_orders(data, market, since, limit) + + async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all trades made by the user + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch trades for + :param int [limit]: the maximum number of trades structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns Trade[]: a list of `trade structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params) + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'uly': currency['id'], + # 'instId': market['id'], + # 'ordId': orderId, + # 'after': billId, + # 'before': billId, + # 'limit': limit, # default 100, max 100 + } + market = None + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + if since is not None: + request['begin'] = since + request, params = self.handle_until_option('end', request, params) + type, query = self.handle_market_type_and_params('fetchMyTrades', market, params) + request['instType'] = self.convert_to_instrument_type(type) + if (limit is not None) and (since is None): # limit = n, okx will return the n most recent results, instead of the n results after limit, so limit should only be sent when since is None + request['limit'] = limit # default 100, max 100 + response = await self.privateGetTradeFillsHistory(self.extend(request, query)) + # + # { + # "code": "0", + # "data": [ + # { + # "side": "buy", + # "fillSz": "0.007533", + # "fillPx": "2654.98", + # "fee": "-0.000007533", + # "ordId": "317321390244397056", + # "instType": "SPOT", + # "instId": "ETH-USDT", + # "clOrdId": "", + # "posSide": "net", + # "billId": "317321390265368576", + # "tag": "0", + # "execType": "T", + # "tradeId": "107601752", + # "feeCcy": "ETH", + # "ts": "1621927314985" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_trades(data, market, since, limit, query) + + async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch all the trades made from a single order + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-get-transaction-details-last-3-months + + :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 ` + """ + request: dict = { + # 'instrument_id': market['id'], + 'ordId': id, + # 'after': '1', # return the page after the specified page number + # 'before': '1', # return the page before the specified page number + # 'limit': limit, # optional, number of results per request, default = maximum = 100 + } + return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params)) + + async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]: + """ + fetch the history of changes, actions done by the user or operations that altered balance of the user + + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + + :param str [code]: unified currency code, default is None + :param int [since]: timestamp in ms of the earliest ledger entry, default is None + :param int [limit]: max number of ledger entries to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict: a `ledger structure ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params) + options = self.safe_dict(self.options, 'fetchLedger', {}) + method = self.safe_string(options, 'method') + method = self.safe_string(params, 'method', method) + params = self.omit(params, 'method') + request: dict = { + # 'instType': None, # 'SPOT', 'MARGIN', 'SWAP', 'FUTURES", 'OPTION' + # 'ccy': None, # currency['id'], + # 'mgnMode': None, # 'isolated', 'cross' + # 'ctType': None, # 'linear', 'inverse', only applicable to FUTURES/SWAP + # 'type': varies depending the 'method' endpoint : + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + # - https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details + # - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + # 'after': 'id', # return records earlier than the requested bill id + # 'before': 'id', # return records newer than the requested bill id + # 'limit': 100, # default 100, max 100 + } + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLedger', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode') + if method != 'privateGetAssetBills': + if marginMode is not None: + request['mgnMode'] = marginMode + type, query = self.handle_market_type_and_params('fetchLedger', None, params) + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + if limit is not None: + request['limit'] = limit + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + request, params = self.handle_until_option('end', request, params) + response = None + if method == 'privateGetAccountBillsArchive': + response = await self.privateGetAccountBillsArchive(self.extend(request, query)) + elif method == 'privateGetAssetBills': + response = await self.privateGetAssetBills(self.extend(request, query)) + else: + response = await self.privateGetAccountBills(self.extend(request, query)) + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # ] + # } + # + # privateGetAssetBills + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_ledger(data, currency, since, limit) + + def parse_ledger_entry_type(self, type): + types: dict = { + '1': 'transfer', # transfer + '2': 'trade', # trade + '3': 'trade', # delivery + '4': 'rebate', # auto token conversion + '5': 'trade', # liquidation + '6': 'transfer', # margin transfer + '7': 'trade', # interest deduction + '8': 'fee', # funding rate + '9': 'trade', # adl + '10': 'trade', # clawback + '11': 'trade', # system token conversion + } + return self.safe_string(types, type, type) + + def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: + # + # privateGetAccountBills, privateGetAccountBillsArchive + # + # { + # "bal": "0.0000819307998198", + # "balChg": "-664.2679586599999802", + # "billId": "310394313544966151", + # "ccy": "USDT", + # "fee": "0", + # "from": "", + # "instId": "LTC-USDT", + # "instType": "SPOT", + # "mgnMode": "cross", + # "notes": "", + # "ordId": "310394313519800320", + # "pnl": "0", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "2", + # "sz": "664.26795866", + # "to": "", + # "ts": "1620275771196", + # "type": "2" + # } + # + # privateGetAssetBills + # + # { + # "billId": "12344", + # "ccy": "BTC", + # "balChg": "2", + # "bal": "12", + # "type": "1", + # "ts": "1597026383085" + # } + # + currencyId = self.safe_string(item, 'ccy') + code = self.safe_currency_code(currencyId, currency) + currency = self.safe_currency(currencyId, currency) + timestamp = self.safe_integer(item, 'ts') + feeCostString = self.safe_string(item, 'fee') + fee = None + if feeCostString is not None: + fee = { + 'cost': self.parse_number(Precise.string_neg(feeCostString)), + 'currency': code, + } + marketId = self.safe_string(item, 'instId') + symbol = self.safe_symbol(marketId, None, '-') + return self.safe_ledger_entry({ + 'info': item, + 'id': self.safe_string(item, 'billId'), + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'account': None, + 'referenceId': self.safe_string(item, 'ordId'), + 'referenceAccount': None, + 'type': self.parse_ledger_entry_type(self.safe_string(item, 'type')), + 'currency': code, + 'symbol': symbol, + 'amount': self.safe_number(item, 'balChg'), + 'before': None, + 'after': self.safe_number(item, 'bal'), + 'status': 'ok', + 'fee': fee, + }, currency) + + def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress: + # + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # } + # + # { + # "ccy":"usdt-erc20", + # "to":"6", + # "addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa", + # "selected":true + # } + # + # { + # "chain": "ETH-OKExChain", + # "addrEx": {"comment": "6040348"}, # some currencies like TON may have self field, + # "ctAddr": "72315c", + # "ccy": "ETH", + # "to": "6", + # "addr": "0x1c9f2244d1ccaa060bd536827c18925db10db102", + # "selected": True + # } + # + address = self.safe_string(depositAddress, 'addr') + tag = self.safe_string_n(depositAddress, ['tag', 'pmtId', 'memo']) + if tag is None: + addrEx = self.safe_value(depositAddress, 'addrEx', {}) + tag = self.safe_string(addrEx, 'comment') + currencyId = self.safe_string(depositAddress, 'ccy') + currency = self.safe_currency(currencyId, currency) + code = currency['code'] + chain = self.safe_string(depositAddress, 'chain') + networks = self.safe_value(currency, 'networks', {}) + networksById = self.index_by(networks, 'id') + networkData = self.safe_value(networksById, chain) + # inconsistent naming responses from exchange + # with respect to network naming provided in currency info vs address chain-names and ids + # + # response from address endpoint: + # { + # "chain": "USDT-Polygon", + # "ctAddr": "", + # "ccy": "USDT", + # "to":"6" , + # "addr": "0x1903441e386cc49d937f6302955b5feb4286dcfa", + # "selected": True + # } + # network information from currency['networks'] field: + # Polygon: { + # info: { + # canDep: False, + # canInternal: False, + # canWd: False, + # ccy: 'USDT', + # chain: 'USDT-Polygon-Bridge', + # mainNet: False, + # maxFee: '26.879528', + # minFee: '13.439764', + # minWd: '0.001', + # name: '' + # }, + # id: 'USDT-Polygon-Bridge', + # network: 'Polygon', + # active: False, + # deposit: False, + # withdraw: False, + # fee: 13.439764, + # precision: None, + # limits: { + # withdraw: { + # min: 0.001, + # max: None + # } + # } + # }, + # + if chain == 'USDT-Polygon': + networkData = self.safe_value_2(networksById, 'USDT-Polygon-Bridge', 'USDT-Polygon') + network = self.safe_string(networkData, 'network') + networkCode = self.network_id_to_code(network, code) + self.check_address(address) + return { + 'info': depositAddress, + 'currency': code, + 'network': networkCode, + 'address': address, + 'tag': tag, + } + + async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]: + """ + fetch a dictionary of addresses for a currency, indexed by network + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code of the currency for the deposit address + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `address structures ` indexed by the network + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = await self.privateGetAssetDepositAddress(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "addr": "okbtothemoon", + # "memo": "971668", # may be missing + # "tag":"52055", # may be missing + # "pmtId": "", # may be missing + # "ccy": "BTC", + # "to": "6", # 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account + # "selected": True + # }, + # # {"ccy":"usdt-erc20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt-trc20","to":"6","addr":"TRrd5SiSZrfQVRKm4e9SRSbn2LNTYqCjqx","selected":true}, + # # {"ccy":"usdt_okexchain","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # # {"ccy":"usdt_kip20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true}, + # ] + # } + # + data = self.safe_list(response, 'data', []) + filtered = self.filter_by(data, 'selected', True) + parsed = self.parse_deposit_addresses(filtered, [currency['code']], False) + return self.index_by(parsed, 'network') + + async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress: + """ + fetch the deposit address for a currency associated with self account + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.network]: the network name for the deposit address + :returns dict: an `address structure ` + """ + await self.load_markets() + rawNetwork = self.safe_string_upper(params, 'network') + params = self.omit(params, 'network') + code = self.safe_currency_code(code) + network = self.network_id_to_code(rawNetwork, code) + response = await self.fetch_deposit_addresses_by_network(code, params) + if network is not None: + result = self.safe_dict(response, network) + if result is None: + raise InvalidAddress(self.id + ' fetchDepositAddress() cannot find ' + network + ' deposit address for ' + code) + return result + codeNetwork = self.network_id_to_code(code, code) + if codeNetwork in response: + return response[codeNetwork] + # if the network is not specified, return the first address + keys = list(response.keys()) + first = self.safe_string(keys, 0) + return self.safe_dict(response, first) + + async def withdraw(self, code: str, amount: float, address: str, tag=None, params={}) -> Transaction: + """ + make a withdrawal + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-withdrawal + + :param str code: unified currency code + :param float amount: the amount to withdraw + :param str address: the address to withdraw to + :param str tag: + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + tag, params = self.handle_withdraw_tag_and_params(tag, params) + self.check_address(address) + await self.load_markets() + currency = self.currency(code) + if (tag is not None) and (len(tag) > 0): + address = address + ':' + tag + request: dict = { + 'ccy': currency['id'], + 'toAddr': address, + 'dest': '4', # 2 = OKCoin International, 3 = OKX 4 = others + 'amt': self.number_to_string(amount), + } + network = self.safe_string(params, 'network') # self line allows the user to specify either ERC20 or ETH + if network is not None: + networks = self.safe_dict(self.options, 'networks', {}) + network = self.safe_string(networks, network.upper(), network) # handle ETH>ERC20 alias + request['chain'] = currency['id'] + '-' + network + params = self.omit(params, 'network') + fee = self.safe_string(params, 'fee') + if fee is None: + currencies = await self.fetch_currencies() + self.currencies = self.deep_extend(self.currencies, currencies) + targetNetwork = self.safe_dict(currency['networks'], self.network_id_to_code(network), {}) + fee = self.safe_string(targetNetwork, 'fee') + if fee is None: + raise ArgumentsRequired(self.id + ' withdraw() requires a "fee" string parameter, network transaction fee must be ≥ 0. Withdrawals to OKCoin or OKX are fee-free, please set "0". Withdrawing to external digital asset address requires network transaction fee.') + request['fee'] = self.number_to_string(fee) # withdrawals to OKCoin or OKX are fee-free, please set 0 + query = self.omit(params, ['fee']) + response = await self.privatePostAssetWithdrawal(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + transaction = self.safe_dict(data, 0) + return self.parse_transaction(transaction, currency) + + async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all deposits made to an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch deposits for + :param int [limit]: the maximum number of deposits structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # 0 waiting for confirmation, 1 deposit credited, 2 deposit successful + # 'after': since, + # 'before' self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetDepositHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # }, + # { + # "amt": "491.6784211", + # "txId": "1744594_3_184_0_asset", + # "ccy": "OKB", + # "from": "", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703809" + # }, + # { + # "amt": "223.18782496", + # "txId": "6d892c669225b1092c780bf0da0c6f912fc7dc8f6b8cc53b003288624c", + # "ccy": "USDT", + # "from": "", + # "to": "39kK4XvgEuM7rX9frgyHoZkWqx4iKu1spD", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703779" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_deposit(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency deposit via the deposit id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-deposit-history + + :param str id: deposit id + :param str code: filter by currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'depId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privateGetAssetDepositHistory(self.extend(request, params)) + data = self.safe_value(response, 'data') + deposit = self.safe_dict(data, 0, {}) + return self.parse_transaction(deposit, currency) + + async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]: + """ + fetch all withdrawals made from an account + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str code: unified currency code + :param int [since]: the earliest time in ms to fetch withdrawals for + :param int [limit]: the maximum number of withdrawals structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: the latest time in ms to fetch entries for + :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params) + :returns dict[]: a list of `transaction structures ` + """ + await self.load_markets() + paginate = False + paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate') + if paginate: + return await self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params) + request: dict = { + # 'ccy': currency['id'], + # 'state': 2, # -3: pending cancel, -2 canceled, -1 failed, 0, pending, 1 sending, 2 sent, 3 awaiting email verification, 4 awaiting manual verification, 5 awaiting identity verification + # 'after': since, + # 'before': self.milliseconds(), + # 'limit': limit, # default 100, max 100 + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = max(since - 1, 0) + if limit is not None: + request['limit'] = limit # default 100, max 100 + request, params = self.handle_until_option('after', request, params) + response = await self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "ts": "1597026383085", + # "state": "2" + # }, + # { + # "amt": "0.01", + # "wdId": "4703879", + # "fee": "0.00000000btc", + # "txId": "", + # "ccy": "BTC", + # "from": "13426335357", + # "to": "13426335357", + # "ts": "1597026383085", + # "state": "2" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_transactions(data, currency, since, limit, params) + + async def fetch_withdrawal(self, id: str, code: Str = None, params={}): + """ + fetch data on a currency withdrawal via the withdrawal id + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-withdrawal-history + + :param str id: withdrawal id + :param str code: unified currency code of the currency withdrawn, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transaction structure ` + """ + await self.load_markets() + request: dict = { + 'wdId': id, + } + currency = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privateGetAssetWithdrawalHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "chain": "USDT-TRC20", + # "clientId": '', + # "fee": "0.8", + # "ccy": "USDT", + # "amt": "54.561", + # "txId": "00cff6ec7fa7c7d7d184bd84e82b9ff36863f07c0421188607f87dfa94e06b70", + # "from": "example@email.com", + # "to": "TEY6qjnKDyyq5jDc3DJizWLCdUySrpQ4yp", + # "state": "2", + # "ts": "1641376485000", + # "wdId": "25147041" + # } + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + withdrawal = self.safe_dict(data, 0, {}) + return self.parse_transaction(withdrawal) + + def parse_transaction_status(self, status: Str): + # + # deposit statuses + # + # { + # "0": "waiting for confirmation", + # "1": "deposit credited", + # "2": "deposit successful" + # } + # + # withdrawal statuses + # + # { + # '-3': "pending cancel", + # "-2": "canceled", + # "-1": "failed", + # "0": "pending", + # "1": "sending", + # "2": "sent", + # "3": "awaiting email verification", + # "4": "awaiting manual verification", + # "5": "awaiting identity verification" + # } + # + statuses: dict = { + '-3': 'pending', + '-2': 'canceled', + '-1': 'failed', + '0': 'pending', + '1': 'pending', + '2': 'ok', + '3': 'pending', + '4': 'pending', + '5': 'pending', + '6': 'pending', + '7': 'pending', + '8': 'pending', + '9': 'pending', + '10': 'pending', + '12': 'pending', + '15': 'pending', + '16': 'pending', + } + return self.safe_string(statuses, status, status) + + def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction: + # + # withdraw + # + # { + # "amt": "0.1", + # "wdId": "67485", + # "ccy": "BTC" + # } + # + # fetchWithdrawals + # + # { + # "amt": "0.094", + # "wdId": "4703879", + # "fee": "0.01000000eth", + # "txId": "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85", + # "ccy": "ETH", + # "from": "13426335357", + # "to": "0xA41446125D0B5b6785f6898c9D67874D763A1519", + # "tag", + # "pmtId", + # "memo", + # "ts": "1597026383085", + # "state": "2" + # } + # + # fetchDeposits + # + # { + # "amt": "0.01044408", + # "txId": "1915737_3_0_0_asset", + # "ccy": "BTC", + # "from": "13801825426", + # "to": "", + # "ts": "1597026383085", + # "state": "2", + # "depId": "4703879" + # } + # + type = None + id = None + withdrawalId = self.safe_string(transaction, 'wdId') + addressFrom = self.safe_string(transaction, 'from') + addressTo = self.safe_string(transaction, 'to') + address = addressTo + tagTo = self.safe_string_2(transaction, 'tag', 'memo') + tagTo = self.safe_string_2(transaction, 'pmtId', tagTo) + if withdrawalId is not None: + type = 'withdrawal' + id = withdrawalId + else: + # the payment_id will appear on new deposits but appears to be removed from the response after 2 months + id = self.safe_string(transaction, 'depId') + type = 'deposit' + currencyId = self.safe_string(transaction, 'ccy') + code = self.safe_currency_code(currencyId) + amount = self.safe_number(transaction, 'amt') + status = self.parse_transaction_status(self.safe_string(transaction, 'state')) + txid = self.safe_string(transaction, 'txId') + timestamp = self.safe_integer(transaction, 'ts') + feeCost = None + if type == 'deposit': + feeCost = 0 + else: + feeCost = self.safe_number(transaction, 'fee') + # todo parse tags + return { + 'info': transaction, + 'id': id, + 'currency': code, + 'amount': amount, + 'network': None, + 'addressFrom': addressFrom, + 'addressTo': addressTo, + 'address': address, + 'tagFrom': None, + 'tagTo': tagTo, + 'tag': tagTo, + 'status': status, + 'type': type, + 'updated': None, + 'txid': txid, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'internal': None, + 'comment': None, + 'fee': { + 'currency': code, + 'cost': feeCost, + }, + } + + async def fetch_leverage(self, symbol: str, params={}) -> Leverage: + """ + fetch the set leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-get-leverage + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage structure ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated') + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + response = await self.privateGetAccountLeverageInfo(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5.00000000", + # "mgnMode": "isolated", + # "posSide": "net" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_leverage(data, market) + + def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage: + marketId = None + marginMode = None + longLeverage = None + shortLeverage = None + for i in range(0, len(leverage)): + entry = leverage[i] + marginMode = self.safe_string_lower(entry, 'mgnMode') + marketId = self.safe_string(entry, 'instId') + positionSide = self.safe_string_lower(entry, 'posSide') + if positionSide == 'long': + longLeverage = self.safe_integer(entry, 'lever') + elif positionSide == 'short': + shortLeverage = self.safe_integer(entry, 'lever') + else: + longLeverage = self.safe_integer(entry, 'lever') + shortLeverage = self.safe_integer(entry, 'lever') + return { + 'info': leverage, + 'symbol': self.safe_symbol(marketId, market), + 'marginMode': marginMode, + 'longLeverage': longLeverage, + 'shortLeverage': shortLeverage, + } + + async def fetch_position(self, symbol: str, params={}): + """ + fetch data on a single open contract trade position + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + :param str symbol: unified market symbol of the market the position is held in, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict: a `position structure ` + """ + await self.load_markets() + market = self.market(symbol) + type, query = self.handle_market_type_and_params('fetchPosition', market, params) + request: dict = { + # instType str No Instrument type, MARGIN, SWAP, FUTURES, OPTION + 'instId': market['id'], + # posId str No Single position ID or multiple position IDs(no more than 20) separated with comma + } + if type is not None: + request['instType'] = self.convert_to_instrument_type(type) + response = await self.privateGetAccountPositions(self.extend(request, query)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + position = self.safe_dict(data, 0) + if position is None: + return None + return self.parse_position(position, market) + + async def fetch_positions(self, symbols: Strings = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history history + + fetch all open positions + :param str[]|None symbols: list of unified market symbols + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION + :returns dict[]: a list of `position structure ` + """ + await self.load_markets() + request: dict = { + # 'instType': 'MARGIN', # optional string, MARGIN, SWAP, FUTURES, OPTION + # 'instId': market['id'], # optional string, e.g. 'BTC-USD-190927-5000-C' + # 'posId': '307173036051017730', # optional string, Single or multiple position IDs(no more than 20) separated with commas + } + if symbols is not None: + marketIds = [] + for i in range(0, len(symbols)): + entry = symbols[i] + market = self.market(entry) + marketIds.append(market['id']) + marketIdsLength = len(marketIds) + if marketIdsLength > 0: + request['instId'] = ','.join(marketIds) + fetchPositionsOptions = self.safe_dict(self.options, 'fetchPositions', {}) + method = self.safe_string(fetchPositionsOptions, 'method', 'privateGetAccountPositions') + response = None + if method == 'privateGetAccountPositionsHistory': + response = await self.privateGetAccountPositionsHistory(self.extend(request, params)) + else: + response = await self.privateGetAccountPositions(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "adl": "1", + # "availPos": "1", + # "avgPx": "2566.31", + # "cTime": "1619507758793", + # "ccy": "ETH", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "", + # "instId": "ETH-USD-210430", + # "instType": "FUTURES", + # "interest": "0", + # "last": "2566.22", + # "lever": "10", + # "liab": "", + # "liabCcy": "", + # "liqPx": "2352.8496681818233", + # "margin": "0.0003896645377994", + # "mgnMode": "isolated", + # "mgnRatio": "11.731726509588816", + # "mmr": "0.0000311811092368", + # "optVal": "", + # "pTime": "1619507761462", + # "pos": "1", + # "posCcy": "", + # "posId": "307173036051017730", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "109844", + # "uTime": "1619507761462", + # "upl": "-0.0000009932766034", + # "uplRatio": "-0.0025490556801078", + # "vegaBS": "", + # "vegaPA": "" + # } + # ] + # } + # + positions = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(positions)): + result.append(self.parse_position(positions[i])) + return self.filter_by_array_positions(result, 'symbol', self.market_symbols(symbols), False) + + async def fetch_positions_for_symbol(self, symbol: str, params={}): + """ + + https://www.okx.com/docs-v5/en/#rest-api-account-get-positions + + fetch all open positions for specific symbol + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.instType]: MARGIN(if needed) + :returns dict[]: a list of `position structure ` + """ + return await self.fetch_positions([symbol], params) + + def parse_position(self, position: dict, market: Market = None): + # + # { + # "adl": "3", + # "availPos": "1", + # "avgPx": "34131.1", + # "cTime": "1627227626502", + # "ccy": "USDT", + # "deltaBS": "", + # "deltaPA": "", + # "gammaBS": "", + # "gammaPA": "", + # "imr": "170.66093041794787", + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "interest": "0", + # "last": "34134.4", + # "lever": "2", + # "liab": "", + # "liabCcy": "", + # "liqPx": "12608.959083877446", + # "markPx": "4786.459271773621", + # "margin": "", + # "mgnMode": "cross", + # "mgnRatio": "140.49930117599155", + # "mmr": "1.3652874433435829", + # "notionalUsd": "341.5130010779638", + # "optVal": "", + # "pos": "1", + # "posCcy": "", + # "posId": "339552508062380036", + # "posSide": "long", + # "thetaBS": "", + # "thetaPA": "", + # "tradeId": "98617799", + # "uTime": "1627227626502", + # "upl": "0.0108608358957281", + # "uplRatio": "0.0000636418743944", + # "vegaBS": "", + # "vegaPA": "" + # } + # history + # { + # "cTime":"1708351230102", + # "ccy":"USDT", + # "closeAvgPx":"1.2567", + # "closeTotalPos":"40", + # "direction":"short", + # "fee":"-0.0351036", + # "fundingFee":"0", + # "instId":"SUSHI-USDT-SWAP", + # "instType":"SWAP", + # "lever":"10.0", + # "liqPenalty":"0", + # "mgnMode":"isolated", + # "openAvgPx":"1.2462", + # "openMaxPos":"40", + # "pnl":"-0.42", + # "pnlRatio":"-0.0912982667308618", + # "posId":"666159086676836352", + # "realizedPnl":"-0.4551036", + # "triggerPx":"", + # "type":"2", + # "uTime":"1708354805699", + # "uly":"SUSHI-USDT" + # } + # + marketId = self.safe_string(position, 'instId') + market = self.safe_market(marketId, market, None, 'contract') + symbol = market['symbol'] + pos = self.safe_string(position, 'pos') # 'pos' field: One way mode: 0 if position is not open, 1 if open | Two way(hedge) mode: -1 if short, 1 if long, 0 if position is not open + contractsAbs = Precise.string_abs(pos) + side = self.safe_string_2(position, 'posSide', 'direction') + hedged = side != 'net' + contracts = self.parse_number(contractsAbs) + if market['margin']: + # margin position + if side == 'net': + posCcy = self.safe_string(position, 'posCcy') + parsedCurrency = self.safe_currency_code(posCcy) + if parsedCurrency is not None: + side = 'long' if (market['base'] == parsedCurrency) else 'short' + if side is None: + side = self.safe_string(position, 'direction') + else: + if pos is not None: + if side == 'net': + if Precise.string_gt(pos, '0'): + side = 'long' + elif Precise.string_lt(pos, '0'): + side = 'short' + else: + side = None + contractSize = self.safe_number(market, 'contractSize') + contractSizeString = self.number_to_string(contractSize) + markPriceString = self.safe_string(position, 'markPx') + notionalString = self.safe_string(position, 'notionalUsd') + if market['inverse']: + notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString) + notional = self.parse_number(notionalString) + marginMode = self.safe_string(position, 'mgnMode') + initialMarginString = None + entryPriceString = self.safe_string_2(position, 'avgPx', 'openAvgPx') + unrealizedPnlString = self.safe_string(position, 'upl') + leverageString = self.safe_string(position, 'lever') + initialMarginPercentage = None + collateralString = None + if marginMode == 'cross': + initialMarginString = self.safe_string(position, 'imr') + collateralString = Precise.string_add(initialMarginString, unrealizedPnlString) + elif marginMode == 'isolated': + initialMarginPercentage = Precise.string_div('1', leverageString) + collateralString = self.safe_string(position, 'margin') + maintenanceMarginString = self.safe_string(position, 'mmr') + maintenanceMargin = self.parse_number(maintenanceMarginString) + maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString) + if initialMarginPercentage is None: + initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4)) + elif initialMarginString is None: + initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString) + rounder = '0.00005' # round to closest 0.01% + maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4)) + liquidationPrice = self.safe_number(position, 'liqPx') + percentageString = self.safe_string(position, 'uplRatio') + percentage = self.parse_number(Precise.string_mul(percentageString, '100')) + timestamp = self.safe_integer(position, 'cTime') + marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4)) + return self.safe_position({ + 'info': position, + 'id': self.safe_string(position, 'posId'), + 'symbol': symbol, + 'notional': notional, + 'marginMode': marginMode, + 'liquidationPrice': liquidationPrice, + 'entryPrice': self.parse_number(entryPriceString), + 'unrealizedPnl': self.parse_number(unrealizedPnlString), + 'realizedPnl': self.safe_number(position, 'realizedPnl'), + 'percentage': percentage, + 'contracts': contracts, + 'contractSize': contractSize, + 'markPrice': self.parse_number(markPriceString), + 'lastPrice': self.safe_number(position, 'closeAvgPx'), + 'side': side, + 'hedged': hedged, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'), + 'maintenanceMargin': maintenanceMargin, + 'maintenanceMarginPercentage': maintenanceMarginPercentage, + 'collateral': self.parse_number(collateralString), + 'initialMargin': self.parse_number(initialMarginString), + 'initialMarginPercentage': self.parse_number(initialMarginPercentage), + 'leverage': self.parse_number(leverageString), + 'marginRatio': marginRatio, + 'stopLossPrice': None, + 'takeProfitPrice': None, + }) + + async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry: + """ + transfer currency internally between wallets on the same account + + https://www.okx.com/docs-v5/en/#rest-api-funding-funds-transfer + + :param str code: unified currency code + :param float amount: amount to transfer + :param str fromAccount: account to transfer from + :param str toAccount: account to transfer to + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `transfer structure ` + """ + await self.load_markets() + currency = self.currency(code) + accountsByType = self.safe_dict(self.options, 'accountsByType', {}) + fromId = self.safe_string(accountsByType, fromAccount, fromAccount) + toId = self.safe_string(accountsByType, toAccount, toAccount) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'type': '0', # 0 = transfer within account by default, 1 = master account to sub-account, 2 = sub-account to master account, 3 = sub-account to master account(Only applicable to APIKey from sub-account), 4 = sub-account to sub-account + 'from': fromId, # remitting account, 6: Funding account, 18: Trading account + 'to': toId, # beneficiary account, 6: Funding account, 18: Trading account + # 'subAcct': 'sub-account-name', # optional, only required when type is 1, 2 or 4 + # 'loanTrans': False, # Whether or not borrowed coins can be transferred out under Multi-currency margin and Portfolio margin. The default is False + # 'clientId': 'client-supplied id', # A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters + # 'omitPosRisk': False, # Ignore position risk. Default is False. Applicable to Portfolio margin + } + if fromId == 'master': + request['type'] = '1' + request['subAcct'] = toId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + elif toId == 'master': + request['type'] = '2' + request['subAcct'] = fromId + request['from'] = self.safe_string(params, 'from', '6') + request['to'] = self.safe_string(params, 'to', '6') + response = await self.privatePostAssetTransfer(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # ] + # } + # + data = self.safe_list(response, 'data', []) + rawTransfer = self.safe_dict(data, 0, {}) + return self.parse_transfer(rawTransfer, currency) + + def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry: + # + # transfer + # + # { + # "transId": "754147", + # "ccy": "USDT", + # "from": "6", + # "amt": "0.1", + # "to": "18" + # } + # + # fetchTransfer + # + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # + # fetchTransfers + # + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", # negative means "to funding", positive meand "from funding" + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # } + # + id = self.safe_string_2(transfer, 'transId', 'billId') + currencyId = self.safe_string(transfer, 'ccy') + code = self.safe_currency_code(currencyId, currency) + amount = self.safe_number(transfer, 'amt') + fromAccountId = self.safe_string(transfer, 'from') + toAccountId = self.safe_string(transfer, 'to') + accountsById = self.safe_dict(self.options, 'accountsById', {}) + timestamp = self.safe_integer(transfer, 'ts') + balanceChange = self.safe_string(transfer, 'sz') + if balanceChange is not None: + amount = self.parse_number(Precise.string_abs(balanceChange)) + return { + 'info': transfer, + 'id': id, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'currency': code, + 'amount': amount, + 'fromAccount': self.safe_string(accountsById, fromAccountId), + 'toAccount': self.safe_string(accountsById, toAccountId), + 'status': self.parse_transfer_status(self.safe_string(transfer, 'state')), + } + + def parse_transfer_status(self, status: Str) -> Str: + statuses: dict = { + 'success': 'ok', + } + return self.safe_string(statuses, status, status) + + async def fetch_transfer(self, id: str, code: Str = None, params={}) -> TransferEntry: + await self.load_markets() + request: dict = { + 'transId': id, + # 'type': 0, # default is 0 transfer within account, 1 master to sub, 2 sub to master + } + response = await self.privateGetAssetTransferState(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "5", + # "ccy": "USDT", + # "from": "18", + # "instId": "", + # "state": "success", + # "subAcct": "", + # "to": "6", + # "toInstId": "", + # "transId": "464424732", + # "type": "0" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + transfer = self.safe_dict(data, 0) + return self.parse_transfer(transfer) + + async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]: + """ + fetch a history of internal transfers made on an account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str code: unified currency code of the currency transferred + :param int [since]: the earliest time in ms to fetch transfers for + :param int [limit]: the maximum number of transfers structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `transfer structures ` + """ + await self.load_markets() + currency = None + request: dict = { + 'type': '1', # https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months + } + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetAccountBillsArchive(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "bal": "70.6874353780312913", + # "balChg": "-4.0000000000000000", + # "billId": "588900695232225299", + # "ccy": "USDT", + # "execType": "", + # "fee": "", + # "from": "18", + # "instId": "", + # "instType": "", + # "mgnMode": "", + # "notes": "To Funding Account", + # "ordId": "", + # "pnl": "", + # "posBal": "", + # "posBalChg": "", + # "price": "0", + # "subType": "12", + # "sz": "-4", + # "to": "6", + # "ts": "1686676866989", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + transfers = self.safe_list(response, 'data', []) + return self.parse_transfers(transfers, currency, since, limit, params) + + def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): + isArray = isinstance(params, list) + request = '/api/' + self.version + '/' + self.implode_params(path, params) + query = self.omit(params, self.extract_params(path)) + url = self.implode_hostname(self.urls['api']['rest']) + request + # type = self.getPathAuthenticationType(path) + if api == 'public': + if query: + url += '?' + self.urlencode(query) + elif api == 'private': + self.check_required_credentials() + # inject id in implicit api call + if method == 'POST' and (path == 'trade/batch-orders' or path == 'trade/order-algo' or path == 'trade/order'): + brokerId = self.safe_string(self.options, 'brokerId', 'e847386590ce4dBC') + if isinstance(params, list): + for i in range(0, len(params)): + entry = params[i] + clientOrderId = self.safe_string(entry, 'clOrdId') + if clientOrderId is None: + entry['clOrdId'] = brokerId + self.uuid16() + entry['tag'] = brokerId + params[i] = entry + else: + clientOrderId = self.safe_string(params, 'clOrdId') + if clientOrderId is None: + params['clOrdId'] = brokerId + self.uuid16() + params['tag'] = brokerId + timestamp = self.iso8601(self.nonce()) + headers = { + 'OK-ACCESS-KEY': self.apiKey, + 'OK-ACCESS-PASSPHRASE': self.password, + 'OK-ACCESS-TIMESTAMP': timestamp, + # 'OK-FROM': '', + # 'OK-TO': '', + # 'OK-LIMIT': '', + } + auth = timestamp + method + request + if method == 'GET': + if query: + urlencodedQuery = '?' + self.urlencode(query) + url += urlencodedQuery + auth += urlencodedQuery + else: + if isArray or query: + body = self.json(query) + auth += body + headers['Content-Type'] = 'application/json' + signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64') + headers['OK-ACCESS-SIGN'] = signature + return {'url': url, 'method': method, 'body': body, 'headers': headers} + + def parse_funding_rate(self, contract, market: Market = None) -> FundingRate: + # + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ws + # { + # "fundingRate":"0.0001875391284828", + # "fundingTime":"1700726400000", + # "instId":"BTC-USD-SWAP", + # "instType":"SWAP", + # "method": "next_period", + # "maxFundingRate":"0.00375", + # "minFundingRate":"-0.00375", + # "nextFundingRate":"0.0002608059239328", + # "nextFundingTime":"1700755200000", + # "premium": "0.0001233824646391", + # "settFundingRate":"0.0001699799259033", + # "settState":"settled", + # "ts":"1700724675402" + # } + # + # in the response above nextFundingRate is actually two funding rates from now + # + nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime') + marketId = self.safe_string(contract, 'instId') + symbol = self.safe_symbol(marketId, market) + nextFundingRate = self.safe_number(contract, 'nextFundingRate') + fundingTime = self.safe_integer(contract, 'fundingTime') + fundingTimeString = self.safe_string(contract, 'fundingTime') + nextFundingTimeString = self.safe_string(contract, 'nextFundingRate') + millisecondsInterval = Precise.string_sub(nextFundingTimeString, fundingTimeString) + # https://www.okx.com/support/hc/en-us/articles/360053909272-Ⅸ-Introduction-to-perpetual-swap-funding-fee + # > The current interest is 0. + return { + 'info': contract, + 'symbol': symbol, + 'markPrice': None, + 'indexPrice': None, + 'interestRate': self.parse_number('0'), + 'estimatedSettlePrice': None, + 'timestamp': None, + 'datetime': None, + 'fundingRate': self.safe_number(contract, 'fundingRate'), + 'fundingTimestamp': fundingTime, + 'fundingDatetime': self.iso8601(fundingTime), + 'nextFundingRate': nextFundingRate, + 'nextFundingTimestamp': nextFundingRateTimestamp, + 'nextFundingDatetime': self.iso8601(nextFundingRateTimestamp), + 'previousFundingRate': None, + 'previousFundingTimestamp': None, + 'previousFundingDatetime': None, + 'interval': self.parse_funding_interval(millisecondsInterval), + } + + def parse_funding_interval(self, interval): + intervals: dict = { + '3600000': '1h', + '14400000': '4h', + '28800000': '8h', + '57600000': '16h', + '86400000': '24h', + } + return self.safe_string(intervals, interval, interval) + + async def fetch_funding_interval(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate interval + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + return await self.fetch_funding_rate(symbol, params) + + async def fetch_funding_rate(self, symbol: str, params={}) -> FundingRate: + """ + fetch the current funding rate + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding rate structure ` + """ + await self.load_markets() + market = self.market(symbol) + if not market['swap']: + raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets') + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetPublicFundingRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "fundingRate": "0.00027815", + # "fundingTime": "1634256000000", + # "instId": "BTC-USD-SWAP", + # "instType": "SWAP", + # "nextFundingRate": "0.00017", + # "nextFundingTime": "1634284800000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + return self.parse_funding_rate(entry, market) + + async def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetch the history of funding payments paid and received on self account + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str symbol: unified market symbol + :param int [since]: the earliest time in ms to fetch funding history for + :param int [limit]: the maximum number of funding history structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `funding history structure ` + """ + await self.load_markets() + request: dict = { + # 'instType': 'SPOT', # SPOT, MARGIN, SWAP, FUTURES, OPTION + # 'ccy': currency['id'], + # 'mgnMode': 'isolated', # isolated, cross + # 'ctType': 'linear', # linear, inverse, only applicable to FUTURES/SWAP + 'type': '8', + # + # supported values for type + # + # 1 Transfer + # 2 Trade + # 3 Delivery + # 4 Auto token conversion + # 5 Liquidation + # 6 Margin transfer + # 7 Interest deduction + # 8 Funding fee + # 9 ADL + # 10 Clawback + # 11 System token conversion + # 12 Strategy transfer + # 13 ddh + # + # 'subType': '', + # + # supported values for subType + # + # 1 Buy + # 2 Sell + # 3 Open long + # 4 Open short + # 5 Close long + # 6 Close short + # 9 Interest deduction + # 11 Transfer in + # 12 Transfer out + # 160 Manual margin increase + # 161 Manual margin decrease + # 162 Auto margin increase + # 110 Auto buy + # 111 Auto sell + # 118 System token conversion transfer in + # 119 System token conversion transfer out + # 100 Partial liquidation close long + # 101 Partial liquidation close short + # 102 Partial liquidation buy + # 103 Partial liquidation sell + # 104 Liquidation long + # 105 Liquidation short + # 106 Liquidation buy + # 107 Liquidation sell + # 110 Liquidation transfer in + # 111 Liquidation transfer out + # 125 ADL close long + # 126 ADL close short + # 127 ADL buy + # 128 ADL sell + # 131 ddh buy + # 132 ddh sell + # 170 Exercised + # 171 Counterparty exercised + # 172 Expired OTM + # 112 Delivery long + # 113 Delivery short + # 117 Delivery/Exercise clawback + # 173 Funding fee expense + # 174 Funding fee income + # 200 System transfer in + # 201 Manually transfer in + # 202 System transfer out + # 203 Manually transfer out + # + # "after": "id", # earlier than the requested bill ID + # "before": "id", # newer than the requested bill ID + # "limit": "100", # default 100, max 100 + } + if limit is not None: + request['limit'] = str(limit) # default 100, max 100 + market = None + if symbol is not None: + market = self.market(symbol) + symbol = market['symbol'] + if market['contract']: + if market['linear']: + request['ctType'] = 'linear' + request['ccy'] = market['quoteId'] + else: + request['ctType'] = 'inverse' + request['ccy'] = market['baseId'] + type, query = self.handle_market_type_and_params('fetchFundingHistory', market, params) + if type == 'swap': + request['instType'] = self.convert_to_instrument_type(type) + # AccountBillsArchive has the same cost but supports three months of data + response = await self.privateGetAccountBillsArchive(self.extend(request, query)) + # + # { + # "bal": "0.0242946200998573", + # "balChg": "0.0000148752712240", + # "billId": "377970609204146187", + # "ccy": "ETH", + # "execType": "", + # "fee": "0", + # "from": "", + # "instId": "ETH-USD-SWAP", + # "instType": "SWAP", + # "mgnMode": "isolated", + # "notes": "", + # "ordId": "", + # "pnl": "0.000014875271224", + # "posBal": "0", + # "posBalChg": "0", + # "subType": "174", + # "sz": "9", + # "to": "", + # "ts": "1636387215588", + # "type": "8" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + timestamp = self.safe_integer(entry, 'ts') + instId = self.safe_string(entry, 'instId') + marketInner = self.safe_market(instId) + currencyId = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(currencyId) + result.append({ + 'info': entry, + 'symbol': marketInner['symbol'], + 'code': code, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string(entry, 'billId'), + 'amount': self.safe_number(entry, 'balChg'), + }) + sorted = self.sort_by(result, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, symbol, since, limit) + + async def set_leverage(self, leverage: Int, symbol: Str = None, params={}): + """ + set the level of leverage for a market + + https://www.okx.com/docs-v5/en/#rest-api-account-set-leverage + + :param float leverage: the rate of leverage + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :param str [params.posSide]: 'long' or 'short' or 'net' for isolated margin long/short mode on futures and swap markets, default is 'net' + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + if (leverage < 1) or (leverage > 125): + raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125') + await self.load_markets() + market = self.market(symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('setLeverage', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated') + request: dict = { + 'lever': leverage, + 'mgnMode': marginMode, + 'instId': market['id'], + } + posSide = self.safe_string(params, 'posSide', 'net') + if marginMode == 'isolated': + if posSide != 'long' and posSide != 'short' and posSide != 'net': + raise BadRequest(self.id + ' setLeverage() requires the posSide argument to be either "long", "short" or "net"') + request['posSide'] = posSide + response = await self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + async def fetch_position_mode(self, symbol: Str = None, params={}): + """ + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-account-configuration + + fetchs the position mode, hedged or one way, hedged for binance is set identically for all linear markets or all inverse markets + :param str symbol: unified symbol of the market to fetch the order book for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.accountId]: if you have multiple accounts, you must specify the account id to fetch the position mode + :returns dict: an object detailing whether the market is in hedged or one-way mode + """ + accounts = await self.fetch_accounts() + length = len(accounts) + selectedAccount = None + if length > 1: + accountId = self.safe_string(params, 'accountId') + if accountId is None: + accountIds = self.get_list_from_object_values(accounts, 'id') + raise ExchangeError(self.id + ' fetchPositionMode() can not detect position mode, because you have multiple accounts. Set params["accountId"] to desired id from: ' + ', '.join(accountIds)) + else: + accountsById = self.index_by(accounts, 'id') + selectedAccount = self.safe_dict(accountsById, accountId) + else: + selectedAccount = accounts[0] + mainAccount = selectedAccount['info'] + posMode = self.safe_string(mainAccount, 'posMode') # long_short_mode, net_mode + isHedged = posMode == 'long_short_mode' + return { + 'info': mainAccount, + 'hedged': isHedged, + } + + async def set_position_mode(self, hedged: bool, symbol: Str = None, params={}): + """ + set hedged to True or False for a market + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-position-mode + + :param bool hedged: set to True to use long_short_mode, False for net_mode + :param str symbol: not used by okx setPositionMode + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: response from the exchange + """ + hedgeMode = None + if hedged: + hedgeMode = 'long_short_mode' + else: + hedgeMode = 'net_mode' + request: dict = { + 'posMode': hedgeMode, + } + response = await self.privatePostAccountSetPositionMode(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "posMode": "net_mode" + # } + # ], + # "msg": "" + # } + # + return response + + async def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}): + """ + set margin mode to 'cross' or 'isolated' + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-set-leverage + + :param str marginMode: 'cross' or 'isolated' + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.leverage]: leverage + :returns dict: response from the exchange + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument') + # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS + # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS + marginMode = marginMode.lower() + if (marginMode != 'cross') and (marginMode != 'isolated'): + raise BadRequest(self.id + ' setMarginMode() marginMode must be either cross or isolated') + await self.load_markets() + market = self.market(symbol) + lever = self.safe_integer_2(params, 'lever', 'leverage') + if (lever is None) or (lever < 1) or (lever > 125): + raise BadRequest(self.id + ' setMarginMode() params["lever"] should be between 1 and 125') + params = self.omit(params, ['leverage']) + request: dict = { + 'lever': lever, + 'mgnMode': marginMode, + 'instId': market['id'], + } + response = await self.privatePostAccountSetLeverage(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "lever": "5", + # "mgnMode": "isolated", + # "posSide": "long" + # } + # ], + # "msg": "" + # } + # + return response + + async def fetch_cross_borrow_rates(self, params={}) -> CrossBorrowRates: + """ + fetch the borrow interest rates of all currencies + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a list of `borrow rate structures ` + """ + await self.load_markets() + response = await self.privateGetAccountInterestRate(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "interestRate": "0.00000833" + # } + # ... + # ], + # } + # + data = self.safe_list(response, 'data', []) + rates = [] + for i in range(0, len(data)): + rates.append(self.parse_borrow_rate(data[i])) + return rates + + async def fetch_cross_borrow_rate(self, code: str, params={}) -> CrossBorrowRate: + """ + fetch the rate of interest to borrow a currency for margin trading + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-interest-rate + + :param str code: unified currency code + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `borrow rate structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + } + response = await self.privateGetAccountInterestRate(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "interestRate": "0.00002065" + # } + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + rate = self.safe_dict(data, 0, {}) + return self.parse_borrow_rate(rate) + + def parse_borrow_rate(self, info, currency: Currency = None): + # + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # } + # + ccy = self.safe_string(info, 'ccy') + timestamp = self.safe_integer(info, 'ts') + return { + 'currency': self.safe_currency_code(ccy), + 'rate': self.safe_number_2(info, 'interestRate', 'rate'), + 'period': 86400000, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': info, + } + + def parse_borrow_rate_histories(self, response, codes, since, limit): + # + # [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ... + # ] + # + borrowRateHistories: dict = {} + for i in range(0, len(response)): + item = response[i] + code = self.safe_currency_code(self.safe_string(item, 'ccy')) + if codes is None or self.in_array(code, codes): + if not (code in borrowRateHistories): + borrowRateHistories[code] = [] + borrowRateStructure = self.parse_borrow_rate(item) + borrrowRateCode = borrowRateHistories[code] + borrrowRateCode.append(borrowRateStructure) + keys = list(borrowRateHistories.keys()) + for i in range(0, len(keys)): + code = keys[i] + borrowRateHistories[code] = self.filter_by_currency_since_limit(borrowRateHistories[code], code, since, limit) + return borrowRateHistories + + async def fetch_borrow_rate_histories(self, codes=None, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a multiple currencies borrow interest rate at specific time slots, returns all currencies if no symbols passed, default is None + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str[]|None codes: list of unified currency codes, default is None + :param int [since]: timestamp in ms of the earliest borrowRate, default is None + :param int [limit]: max number of borrow rate prices to return, default is None + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a dictionary of `borrow rate structures ` indexed by the market symbol + """ + await self.load_markets() + request: dict = { + # 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_histories(data, codes, since, limit) + + async def fetch_borrow_rate_history(self, code: str, since: Int = None, limit: Int = None, params={}): + """ + retrieves a history of a currencies borrow interest rate at specific time slots + + https://www.okx.com/docs-v5/en/#financial-product-savings-get-public-borrow-history-public + + :param str code: unified currency code + :param int [since]: timestamp for the earliest borrow rate + :param int [limit]: the maximum number of `borrow rate structures ` to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: an array of `borrow rate structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + # 'after': self.milliseconds(), # Pagination of data to return records earlier than the requested ts, + # 'before': since, # Pagination of data to return records newer than the requested ts, + # 'limit': limit, # default is 100 and maximum is 100 + } + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetFinanceSavingsLendingRateHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "992.10341195", + # "ccy": "BTC", + # "rate": "0.01", + # "ts": "1643954400000" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_borrow_rate_history(data, code, since, limit) + + async def modify_margin_helper(self, symbol: str, amount, type, params={}) -> MarginModification: + await self.load_markets() + market = self.market(symbol) + posSide = self.safe_string(params, 'posSide', 'net') + params = self.omit(params, ['posSide']) + request: dict = { + 'instId': market['id'], + 'amt': amount, + 'type': type, + 'posSide': posSide, + } + response = await self.privatePostAccountPositionMarginBalance(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + entry = self.safe_dict(data, 0, {}) + errorCode = self.safe_string(response, 'code') + return self.extend(self.parse_margin_modification(entry, market), { + 'status': 'ok' if (errorCode == '0') else 'failed', + }) + + def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification: + # + # addMargin/reduceMargin + # + # { + # "amt": "0.01", + # "instId": "ETH-USD-SWAP", + # "posSide": "net", + # "type": "reduce" + # } + # + # fetchMarginAdjustmentHistory + # + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # + amountRaw = self.safe_string_2(data, 'amt', 'posBalChg') + typeRaw = self.safe_string(data, 'type') + type = None + if typeRaw == '6': + type = 'add' if Precise.string_gt(amountRaw, '0') else 'reduce' + else: + type = typeRaw + amount = Precise.string_abs(amountRaw) + marketId = self.safe_string(data, 'instId') + responseMarket = self.safe_market(marketId, market) + code = responseMarket['base'] if responseMarket['inverse'] else responseMarket['quote'] + timestamp = self.safe_integer(data, 'ts') + return { + 'info': data, + 'symbol': responseMarket['symbol'], + 'type': type, + 'marginMode': 'isolated', + 'amount': self.parse_number(amount), + 'code': code, + 'total': None, + 'status': None, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + } + + async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + remove margin from a position + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: the amount of margin to remove + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'reduce', params) + + async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification: + """ + add margin + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-increase-decrease-margin + + :param str symbol: unified market symbol + :param float amount: amount of margin to add + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin structure ` + """ + return await self.modify_margin_helper(symbol, amount, 'add', params) + + async def fetch_market_leverage_tiers(self, symbol: str, params={}) -> List[LeverageTier]: + """ + retrieve information on the maximum leverage, and maintenance margin for trades of varying trade sizes for a single market + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-position-tiers + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict: a `leverage tiers structure ` + """ + await self.load_markets() + market = self.market(symbol) + type = 'MARGIN' if market['spot'] else self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + if not uly: + if type != 'MARGIN': + raise BadRequest(self.id + ' fetchMarketLeverageTiers() cannot fetch leverage tiers for ' + symbol) + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchMarketLeverageTiers', params) + if marginMode is None: + marginMode = self.safe_string(params, 'tdMode', 'cross') # cross marginMode + request: dict = { + 'instType': type, + 'tdMode': marginMode, + 'uly': uly, + } + if type == 'MARGIN': + request['instId'] = market['id'] + response = await self.publicGetPublicPositionTiers(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_market_leverage_tiers(data, market) + + def parse_market_leverage_tiers(self, info, market: Market = None) -> List[LeverageTier]: + """ + @ignore + :param dict info: Exchange response for 1 market + :param dict market: CCXT market + """ + # + # [ + # { + # "baseMaxLoan": "500", + # "imr": "0.1", + # "instId": "ETH-USDT", + # "maxLever": "10", + # "maxSz": "500", + # "minSz": "0", + # "mmr": "0.03", + # "optMgnFactor": "0", + # "quoteMaxLoan": "200000", + # "tier": "1", + # "uly": "" + # }, + # ... + # ] + # + tiers = [] + for i in range(0, len(info)): + tier = info[i] + marketId = self.safe_string(tier, 'instId') + tiers.append({ + 'tier': self.safe_integer(tier, 'tier'), + 'symbol': self.safe_symbol(marketId, market), + 'currency': market['quote'], + 'minNotional': self.safe_number(tier, 'minSz'), + 'maxNotional': self.safe_number(tier, 'maxSz'), + 'maintenanceMarginRate': self.safe_number(tier, 'mmr'), + 'maxLeverage': self.safe_number(tier, 'maxLever'), + 'info': tier, + }) + return tiers + + async def fetch_borrow_interest(self, code: Str = None, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[BorrowInterest]: + """ + fetch the interest owed by the user for borrowing currency for margin trading + + https://www.okx.com/docs-v5/en/#rest-api-account-get-interest-accrued-data + + :param str code: the unified currency code for the currency of the interest + :param str symbol: the market symbol of an isolated margin market, if None, the interest for cross margin markets is returned + :param int [since]: timestamp in ms of the earliest time to receive interest records for + :param int [limit]: the number of `borrow interest structures ` to retrieve + :param dict [params]: exchange specific parameters + :param int [params.type]: Loan type 1 - VIP loans 2 - Market loans *Default is Market loans* + :param str [params.marginMode]: 'cross' or 'isolated' + :returns dict[]: An list of `borrow interest structures ` + """ + await self.load_markets() + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('fetchBorrowInterest', params) + if marginMode is None: + marginMode = self.safe_string(params, 'mgnMode', 'cross') # cross marginMode + request: dict = { + 'mgnMode': marginMode, + } + market = None + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + if symbol is not None: + market = self.market(symbol) + request['instId'] = market['id'] + response = await self.privateGetAccountInterestAccrued(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "USDT", + # "instId": "", + # "interest": "0.0003960833333334", + # "interestRate": "0.0000040833333333", + # "liab": "97", + # "mgnMode": "", + # "ts": "1637312400000", + # "type": "1" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + interest = self.parse_borrow_interests(data) + return self.filter_by_currency_since_limit(interest, code, since, limit) + + def parse_borrow_interest(self, info: dict, market: Market = None) -> BorrowInterest: + instId = self.safe_string(info, 'instId') + if instId is not None: + market = self.safe_market(instId, market) + timestamp = self.safe_integer(info, 'ts') + return { + 'info': info, + 'symbol': self.safe_string(market, 'symbol'), + 'currency': self.safe_currency_code(self.safe_string(info, 'ccy')), + 'interest': self.safe_number(info, 'interest'), + 'interestRate': self.safe_number(info, 'interestRate'), + 'amountBorrowed': self.safe_number(info, 'liab'), + 'marginMode': self.safe_string(info, 'mgnMode'), + 'timestamp': timestamp, # Interest accrued time + 'datetime': self.iso8601(timestamp), + } + + async def borrow_cross_margin(self, code: str, amount: float, params={}): + """ + create a loan to borrow margin(need to be VIP 5 and above) + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to borrow + :param float amount: the amount to borrow + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'borrow', + } + response = await self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "borrow", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + async def repay_cross_margin(self, code: str, amount, params={}): + """ + repay borrowed margin and interest + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-vip-loans-borrow-and-repay + + :param str code: unified currency code of the currency to repay + :param float amount: the amount to repay + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.id]: the order ID of borrowing, it is necessary while repaying + :returns dict: a `margin loan structure ` + """ + await self.load_markets() + id = self.safe_string_2(params, 'id', 'ordId') + params = self.omit(params, 'id') + if id is None: + raise ArgumentsRequired(self.id + ' repayCrossMargin() requires an id parameter') + currency = self.currency(code) + request: dict = { + 'ccy': currency['id'], + 'amt': self.currency_to_precision(code, amount), + 'side': 'repay', + 'ordId': id, + } + response = await self.privatePostAccountBorrowRepay(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "amt": "102", + # "ccy": "USDT", + # "ordId": "544199684697214976", + # "side": "repay", + # "state": "1" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + loan = self.safe_dict(data, 0, {}) + return self.parse_margin_loan(loan, currency) + + def parse_margin_loan(self, info, currency: Currency = None): + # + # { + # "amt": "102", + # "availLoan": "97", + # "ccy": "USDT", + # "loanQuota": "6000000", + # "posLoan": "0", + # "side": "repay", + # "usedLoan": "97" + # } + # + currencyId = self.safe_string(info, 'ccy') + return { + 'id': None, + 'currency': self.safe_currency_code(currencyId, currency), + 'amount': self.safe_number(info, 'amt'), + 'symbol': None, + 'timestamp': None, + 'datetime': None, + 'info': info, + } + + async def fetch_open_interest(self, symbol: str, params={}): + """ + Retrieves the open interest of a currency + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-open-interest + + :param str symbol: Unified CCXT market symbol + :param dict [params]: exchange specific parameters + :returns dict} an open interest structure{@link https://docs.ccxt.com/#/?id=open-interest-structure: + """ + await self.load_markets() + market = self.market(symbol) + if not market['contract']: + raise BadRequest(self.id + ' fetchOpenInterest() supports contract markets only') + type = self.convert_to_instrument_type(market['type']) + uly = self.safe_string(market['info'], 'uly') + request: dict = { + 'instType': type, + 'uly': uly, + 'instId': market['id'], + } + response = await self.publicGetPublicOpenInterest(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "instId": "BTC-USDT-SWAP", + # "instType": "SWAP", + # "oi": "2125419", + # "oiCcy": "21254.19", + # "ts": "1664005108969" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interest(data[0], market) + + async def fetch_open_interest_history(self, symbol: str, timeframe='1d', since: Int = None, limit: Int = None, params={}): + """ + Retrieves the open interest history of a currency + + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-contracts-open-interest-and-volume + https://www.okx.com/docs-v5/en/#rest-api-trading-data-get-options-open-interest-and-volume + + :param str symbol: Unified CCXT currency code or unified symbol + :param str timeframe: "5m", "1h", or "1d" for option only "1d" or "8h" + :param int [since]: The time in ms of the earliest record to retrieve unix timestamp + :param int [limit]: Not used by okx, but parsed internally by CCXT + :param dict [params]: Exchange specific parameters + :param int [params.until]: The time in ms of the latest record to retrieve unix timestamp + :returns: An array of `open interest structures ` + """ + options = self.safe_dict(self.options, 'fetchOpenInterestHistory', {}) + timeframes = self.safe_dict(options, 'timeframes', {}) + timeframe = self.safe_string(timeframes, timeframe, timeframe) + if timeframe != '5m' and timeframe != '1H' and timeframe != '1D': + raise BadRequest(self.id + ' fetchOpenInterestHistory cannot only use the 5m, 1h, and 1d timeframe') + await self.load_markets() + # handle unified currency code or symbol + currencyId = None + market = None + if (symbol in self.markets) or (symbol in self.markets_by_id): + market = self.market(symbol) + currencyId = market['baseId'] + else: + currency = self.currency(symbol) + currencyId = currency['id'] + request: dict = { + 'ccy': currencyId, + 'period': timeframe, + } + type = None + response = None + type, params = self.handle_market_type_and_params('fetchOpenInterestHistory', market, params) + if type == 'option': + response = await self.publicGetRubikStatOptionOpenInterestVolume(self.extend(request, params)) + else: + if since is not None: + request['begin'] = since + until = self.safe_integer(params, 'until') + if until is not None: + request['end'] = until + params = self.omit(params, ['until']) + response = await self.publicGetRubikStatContractsOpenInterestVolume(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) + # "74285877.617", # volume(USD) + # ], + # ... + # ], + # "msg": '' + # } + # + data = self.safe_list(response, 'data', []) + return self.parse_open_interests_history(data, None, since, limit) + + def parse_open_interest(self, interest, market: Market = None): + # + # fetchOpenInterestHistory + # + # [ + # "1648221300000", # timestamp + # "2183354317.945", # open interest(USD) - (coin) for options + # "74285877.617", # volume(USD) - (coin) for options + # ] + # + # fetchOpenInterest + # + # { + # "instId": "BTC-USD-230520-25500-P", + # "instType": "OPTION", + # "oi": "300", + # "oiCcy": "3", + # "oiUsd": "3", + # "ts": "1684551166251" + # } + # + id = self.safe_string(interest, 'instId') + market = self.safe_market(id, market) + time = self.safe_integer(interest, 'ts') + timestamp = self.safe_integer(interest, 0, time) + baseVolume = None + quoteVolume = None + openInterestAmount = None + openInterestValue = None + type = self.safe_string(self.options, 'defaultType') + if isinstance(interest, list): + if type == 'option': + openInterestAmount = self.safe_number(interest, 1) + baseVolume = self.safe_number(interest, 2) + else: + openInterestValue = self.safe_number(interest, 1) + quoteVolume = self.safe_number(interest, 2) + else: + baseVolume = self.safe_number(interest, 'oiCcy') + openInterestAmount = self.safe_number(interest, 'oi') + openInterestValue = self.safe_number(interest, 'oiUsd') + return self.safe_open_interest({ + 'symbol': self.safe_symbol(id), + 'baseVolume': baseVolume, # deprecated + 'quoteVolume': quoteVolume, # deprecated + 'openInterestAmount': openInterestAmount, + 'openInterestValue': openInterestValue, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'info': interest, + }, market) + + def set_sandbox_mode(self, enable: bool): + super(okx, self).set_sandbox_mode(enable) + self.options['sandboxMode'] = enable + if enable: + self.headers['x-simulated-trading'] = '1' + elif 'x-simulated-trading' in self.headers: + self.headers = self.omit(self.headers, 'x-simulated-trading') + + async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}): + """ + fetch deposit and withdraw fees + + https://www.okx.com/docs-v5/en/#rest-api-funding-get-currencies + + :param str[]|None codes: list of unified currency codes + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict[]: a list of `fees structures ` + """ + await self.load_markets() + request = {} + if codes is not None: + ids = self.currency_ids(codes) + request['ccy'] = ','.join(ids) + response = await self.privateGetAssetCurrencies(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-ERC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "16", + # "maxWd": "8852150", + # "minFee": "8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # }, + # ... + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data') + return self.parse_deposit_withdraw_fees(data, codes) + + def parse_deposit_withdraw_fees(self, response, codes=None, currencyIdKey=None): + # + # [ + # { + # "canDep": True, + # "canInternal": False, + # "canWd": True, + # "ccy": "USDT", + # "chain": "USDT-TRC20", + # "logoLink": "https://static.coinall.ltd/cdn/assets/imgs/221/5F74EB20302D7761.png", + # "mainNet": False, + # "maxFee": "1.6", + # "maxWd": "8852150", + # "minFee": "0.8", + # "minWd": "2", + # "name": "Tether", + # "usedWdQuota": "0", + # "wdQuota": "500", + # "wdTickSz": "3" + # } + # ] + # + depositWithdrawFees: dict = {} + codes = self.market_codes(codes) + for i in range(0, len(response)): + feeInfo = response[i] + currencyId = self.safe_string(feeInfo, 'ccy') + code = self.safe_currency_code(currencyId) + if (codes is None) or (self.in_array(code, codes)): + depositWithdrawFee = self.safe_value(depositWithdrawFees, code) + if depositWithdrawFee is None: + depositWithdrawFees[code] = self.deposit_withdraw_fee({}) + depositWithdrawFees[code]['info'][currencyId] = feeInfo + chain = self.safe_string(feeInfo, 'chain') + if chain is None: + continue + chainSplit = chain.split('-') + networkId = self.safe_value(chainSplit, 1) + withdrawFee = self.safe_number(feeInfo, 'fee') + withdrawResult: dict = { + 'fee': withdrawFee, + 'percentage': False if (withdrawFee is not None) else None, + } + depositResult: dict = { + 'fee': None, + 'percentage': None, + } + networkCode = self.network_id_to_code(networkId, code) + depositWithdrawFees[code]['networks'][networkCode] = { + 'withdraw': withdrawResult, + 'deposit': depositResult, + } + depositWithdrawCodes = list(depositWithdrawFees.keys()) + for i in range(0, len(depositWithdrawCodes)): + code = depositWithdrawCodes[i] + currency = self.currency(code) + depositWithdrawFees[code] = self.assign_default_deposit_withdraw_fees(depositWithdrawFees[code], currency) + return depositWithdrawFees + + async def fetch_settlement_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): + """ + fetches historical settlement records + + https://www.okx.com/docs-v5/en/#rest-api-public-data-get-delivery-exercise-history + + :param str symbol: unified market symbol to fetch the settlement history for + :param int [since]: timestamp in ms + :param int [limit]: number of records + :param dict [params]: exchange specific params + :returns dict[]: a list of `settlement history objects ` + """ + if symbol is None: + raise ArgumentsRequired(self.id + ' fetchSettlementHistory() requires a symbol argument') + await self.load_markets() + market = self.market(symbol) + type = None + type, params = self.handle_market_type_and_params('fetchSettlementHistory', market, params) + if type != 'future' and type != 'option': + raise NotSupported(self.id + ' fetchSettlementHistory() supports futures and options markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(type), + 'uly': market['baseId'] + '-' + market['quoteId'], + } + if since is not None: + request['before'] = since - 1 + if limit is not None: + request['limit'] = limit + response = await self.publicGetPublicDeliveryExerciseHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + settlements = self.parse_settlements(data, market) + sorted = self.sort_by(settlements, 'timestamp') + return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit) + + def parse_settlement(self, settlement, market): + # + # { + # "insId": "BTC-USD-230521-28500-P", + # "px": "27081.2007345984751516", + # "type": "exercised" + # } + # + marketId = self.safe_string(settlement, 'insId') + return { + 'info': settlement, + 'symbol': self.safe_symbol(marketId, market), + 'price': self.safe_number(settlement, 'px'), + 'timestamp': None, + 'datetime': None, + } + + def parse_settlements(self, settlements, market): + # + # { + # "details": [ + # { + # "insId": "BTC-USD-230523-25750-C", + # "px": "27290.1486867000556483", + # "type": "exercised" + # }, + # ], + # "ts":"1684656000000" + # } + # + result = [] + for i in range(0, len(settlements)): + entry = settlements[i] + timestamp = self.safe_integer(entry, 'ts') + details = self.safe_list(entry, 'details', []) + for j in range(0, len(details)): + settlement = self.parse_settlement(details[j], market) + result.append(self.extend(settlement, { + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + })) + return result + + async def fetch_underlying_assets(self, params={}): + """ + fetches the market ids of underlying assets for a specific contract market type + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-underlying + + :param dict [params]: exchange specific params + :param str [params.type]: the contract market type, 'option', 'swap' or 'future', the default is 'option' + :returns dict[]: a list of `underlying assets ` + """ + await self.load_markets() + marketType = None + marketType, params = self.handle_market_type_and_params('fetchUnderlyingAssets', None, params) + if (marketType is None) or (marketType == 'spot'): + marketType = 'option' + if (marketType != 'option') and (marketType != 'swap') and (marketType != 'future'): + raise NotSupported(self.id + ' fetchUnderlyingAssets() supports contract markets only') + request: dict = { + 'instType': self.convert_to_instrument_type(marketType), + } + response = await self.publicGetPublicUnderlying(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # [ + # "BTC-USD", + # "ETH-USD" + # ] + # ], + # "msg": "" + # } + # + underlyings = self.safe_list(response, 'data', []) + return underlyings[0] + + async def fetch_greeks(self, symbol: str, params={}) -> Greeks: + """ + fetches an option contracts greeks, financial metrics used to measure the factors that affect the price of an options contract + + https://www.okx.com/docs-v5/en/#public-data-rest-api-get-option-market-data + + :param str symbol: unified symbol of the market to fetch greeks for + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `greeks structure ` + """ + await self.load_markets() + market = self.market(symbol) + marketId = market['id'] + optionParts = marketId.split('-') + request: dict = { + 'uly': market['info']['uly'], + 'instFamily': market['info']['instFamily'], + 'expTime': self.safe_string(optionParts, 2), + } + response = await self.publicGetPublicOptSummary(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # }, + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + entryMarketId = self.safe_string(entry, 'instId') + if entryMarketId == marketId: + return self.parse_greeks(entry, market) + return None + + def parse_greeks(self, greeks: dict, market: Market = None) -> Greeks: + # + # { + # "askVol": "0", + # "bidVol": "0", + # "delta": "0.5105464486882039", + # "deltaBS": "0.7325502184143025", + # "fwdPx": "37675.80158694987186", + # "gamma": "-0.13183515090501083", + # "gammaBS": "0.000024139685826358558", + # "instId": "BTC-USD-240329-32000-C", + # "instType": "OPTION", + # "lever": "4.504428015946619", + # "markVol": "0.5916253554539876", + # "realVol": "0", + # "theta": "-0.0004202992014012855", + # "thetaBS": "-18.52354631567909", + # "ts": "1699586421976", + # "uly": "BTC-USD", + # "vega": "0.0020207455080045846", + # "vegaBS": "74.44022302387287", + # "volLv": "0.5948549730405797" + # } + # + timestamp = self.safe_integer(greeks, 'ts') + marketId = self.safe_string(greeks, 'instId') + symbol = self.safe_symbol(marketId, market) + return { + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'delta': self.safe_number(greeks, 'delta'), + 'gamma': self.safe_number(greeks, 'gamma'), + 'theta': self.safe_number(greeks, 'theta'), + 'vega': self.safe_number(greeks, 'vega'), + 'rho': None, + 'bidSize': None, + 'askSize': None, + 'bidImpliedVolatility': self.safe_number(greeks, 'bidVol'), + 'askImpliedVolatility': self.safe_number(greeks, 'askVol'), + 'markImpliedVolatility': self.safe_number(greeks, 'markVol'), + 'bidPrice': None, + 'askPrice': None, + 'markPrice': None, + 'lastPrice': None, + 'underlyingPrice': None, + 'info': greeks, + } + + async def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order: + """ + closes open positions for a market + + https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-close-positions + + :param str symbol: Unified CCXT market symbol + :param str [side]: 'buy' or 'sell', leave in net mode + :param dict [params]: extra parameters specific to the okx api endpoint + :param str [params.clientOrderId]: a unique identifier for the order + :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross + :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency + + EXCHANGE SPECIFIC PARAMETERS + :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False + :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters + :returns dict[]: `A list of position structures ` + """ + await self.load_markets() + market = self.market(symbol) + clientOrderId = self.safe_string(params, 'clientOrderId') + code = self.safe_string(params, 'code') + marginMode = None + marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross') + request: dict = { + 'instId': market['id'], + 'mgnMode': marginMode, + } + if side is not None: + if (side == 'buy'): + request['posSide'] = 'long' + elif side == 'sell': + request['posSide'] = 'short' + else: + request['posSide'] = side + if clientOrderId is not None: + request['clOrdId'] = clientOrderId + if code is not None: + currency = self.currency(code) + request['ccy'] = currency['id'] + response = await self.privatePostTradeClosePosition(self.extend(request, params)) + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId":"e847386590ce4dBCe903bbc394dc88bf", + # "ordId":"", + # "sCode":"51000", + # "sMsg":"Parameter posSide error ", + # "tag":"e847386590ce4dBC" + # } + # ], + # "inTime": "1701877077101064", + # "msg": "All operations failed", + # "outTime": "1701877077102579" + # } + # + data = self.safe_list(response, 'data', []) + order = self.safe_dict(data, 0) + return self.parse_order(order, market) + + async def fetch_option(self, symbol: str, params={}) -> Option: + """ + fetches option data that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-ticker + + :param str symbol: unified market symbol + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an `option chain structure ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + response = await self.publicGetMarketTicker(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # ] + # } + # + result = self.safe_list(response, 'data', []) + chain = self.safe_dict(result, 0, {}) + return self.parse_option(chain, None, market) + + async def fetch_option_chain(self, code: str, params={}) -> OptionChain: + """ + fetches data for an underlying asset that is commonly found in an option chain + + https://www.okx.com/docs-v5/en/#order-book-trading-market-data-get-tickers + + :param str code: base currency to fetch an option chain for + :param dict [params]: extra parameters specific to the exchange API endpoint + :param str [params.uly]: the underlying asset, can be obtained from fetchUnderlyingAssets() + :returns dict: a list of `option chain structures ` + """ + await self.load_markets() + currency = self.currency(code) + request: dict = { + 'uly': currency['code'] + '-USD', + 'instType': 'OPTION', + } + response = await self.publicGetMarketTickers(self.extend(request, params)) + # + # { + # "code": "0", + # "msg": "", + # "data": [ + # { + # "instType": "OPTION", + # "instId": "BTC-USD-240323-52000-C", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176207008", + # "sodUtc0": "", + # "sodUtc8": "" + # }, + # ] + # } + # + result = self.safe_list(response, 'data', []) + return self.parse_option_chain(result, None, 'instId') + + def parse_option(self, chain: dict, currency: Currency = None, market: Market = None) -> Option: + # + # { + # "instType": "OPTION", + # "instId": "BTC-USD-241227-60000-P", + # "last": "", + # "lastSz": "0", + # "askPx": "", + # "askSz": "0", + # "bidPx": "", + # "bidSz": "0", + # "open24h": "", + # "high24h": "", + # "low24h": "", + # "volCcy24h": "0", + # "vol24h": "0", + # "ts": "1711176035035", + # "sodUtc0": "", + # "sodUtc8": "" + # } + # + marketId = self.safe_string(chain, 'instId') + market = self.safe_market(marketId, market) + timestamp = self.safe_integer(chain, 'ts') + return { + 'info': chain, + 'currency': None, + 'symbol': market['symbol'], + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'impliedVolatility': None, + 'openInterest': None, + 'bidPrice': self.safe_number(chain, 'bidPx'), + 'askPrice': self.safe_number(chain, 'askPx'), + 'midPrice': None, + 'markPrice': None, + 'lastPrice': self.safe_number(chain, 'last'), + 'underlyingPrice': None, + 'change': None, + 'percentage': None, + 'baseVolume': self.safe_number(chain, 'volCcy24h'), + 'quoteVolume': None, + } + + async def fetch_convert_quote(self, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + fetch a quote for converting from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-estimate-quote + + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'baseCcy': fromCode.upper(), + 'quoteCcy': toCode.upper(), + 'rfqSzCcy': fromCode.upper(), + 'rfqSz': self.number_to_string(amount), + 'side': 'sell', + } + response = await self.privatePostAssetConvertEstimateQuote(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def create_convert_trade(self, id: str, fromCode: str, toCode: str, amount: Num = None, params={}) -> Conversion: + """ + convert from one currency to another + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-convert-trade + + :param str id: the id of the trade that you want to make + :param str fromCode: the currency that you want to sell and convert from + :param str toCode: the currency that you want to buy and convert into + :param float [amount]: how much you want to trade in units of the from currency + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'quoteId': id, + 'baseCcy': fromCode, + 'quoteCcy': toCode, + 'szCcy': fromCode, + 'sz': self.number_to_string(amount), + 'side': 'sell', + } + response = await self.privatePostAssetConvertTrade(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy', fromCode) + fromCurrency = self.currency(fromCurrencyId) + toCurrencyId = self.safe_string(result, 'quoteCcy', toCode) + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def fetch_convert_trade(self, id: str, code: Str = None, params={}) -> Conversion: + """ + fetch the data for a conversion trade + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str id: the id of the trade that you want to fetch + :param str [code]: the unified currency code of the conversion trade + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: a `conversion structure ` + """ + await self.load_markets() + request: dict = { + 'clTReqId': id, + } + response = await self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = self.safe_dict(data, 0, {}) + fromCurrencyId = self.safe_string(result, 'baseCcy') + toCurrencyId = self.safe_string(result, 'quoteCcy') + fromCurrency = None + toCurrency = None + if fromCurrencyId is not None: + fromCurrency = self.currency(fromCurrencyId) + if toCurrencyId is not None: + toCurrency = self.currency(toCurrencyId) + return self.parse_conversion(result, fromCurrency, toCurrency) + + async def fetch_convert_trade_history(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Conversion]: + """ + fetch the users history of conversion trades + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-history + + :param str [code]: the unified currency code + :param int [since]: the earliest time in ms to fetch conversions for + :param int [limit]: the maximum number of conversion structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest conversion to fetch + :returns dict[]: a list of `conversion structures ` + """ + await self.load_markets() + request: dict = {} + request, params = self.handle_until_option('after', request, params) + if since is not None: + request['before'] = since + if limit is not None: + request['limit'] = limit + response = await self.privateGetAssetConvertHistory(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # ], + # "msg": "" + # } + # + rows = self.safe_list(response, 'data', []) + return self.parse_conversions(rows, code, 'baseCcy', 'quoteCcy', since, limit) + + def parse_conversion(self, conversion: dict, fromCurrency: Currency = None, toCurrency: Currency = None) -> Conversion: + # + # fetchConvertQuote + # + # { + # "baseCcy": "ETH", + # "baseSz": "0.01023052", + # "clQReqId": "", + # "cnvtPx": "2932.40104429", + # "origRfqSz": "30", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "quoteSz": "30", + # "quoteTime": "1646188510461", + # "rfqSz": "30", + # "rfqSzCcy": "USDT", + # "side": "buy", + # "ttlMs": "10000" + # } + # + # createConvertTrade + # + # { + # "baseCcy": "ETH", + # "clTReqId": "", + # "fillBaseSz": "0.01023052", + # "fillPx": "2932.40104429", + # "fillQuoteSz": "30", + # "instId": "ETH-USDT", + # "quoteCcy": "USDT", + # "quoteId": "quoterETH-USDT16461885104612381", + # "side": "buy", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "ts": "1646188520338" + # } + # + # fetchConvertTrade, fetchConvertTradeHistory + # + # { + # "clTReqId": "", + # "instId": "ETH-USDT", + # "side": "buy", + # "fillPx": "2932.401044", + # "baseCcy": "ETH", + # "quoteCcy": "USDT", + # "fillBaseSz": "0.01023052", + # "state": "fullyFilled", + # "tradeId": "trader16461885203381437", + # "fillQuoteSz": "30", + # "ts": "1646188520000" + # } + # + timestamp = self.safe_integer_2(conversion, 'quoteTime', 'ts') + fromCoin = self.safe_string(conversion, 'baseCcy') + fromCode = self.safe_currency_code(fromCoin, fromCurrency) + to = self.safe_string(conversion, 'quoteCcy') + toCode = self.safe_currency_code(to, toCurrency) + return { + 'info': conversion, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'id': self.safe_string_n(conversion, ['clQReqId', 'tradeId', 'quoteId']), + 'fromCurrency': fromCode, + 'fromAmount': self.safe_number_2(conversion, 'baseSz', 'fillBaseSz'), + 'toCurrency': toCode, + 'toAmount': self.safe_number_2(conversion, 'quoteSz', 'fillQuoteSz'), + 'price': self.safe_number_2(conversion, 'cnvtPx', 'fillPx'), + 'fee': None, + } + + async def fetch_convert_currencies(self, params={}) -> Currencies: + """ + fetches all available currencies that can be converted + + https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-convert-currencies + + :param dict [params]: extra parameters specific to the exchange API endpoint + :returns dict: an associative dictionary of currencies + """ + await self.load_markets() + response = await self.privateGetAssetConvertCurrencies(params) + # + # { + # "code": "0", + # "data": [ + # { + # "ccy": "BTC", + # "max": "", + # "min": "" + # }, + # ], + # "msg": "" + # } + # + result: dict = {} + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + entry = data[i] + id = self.safe_string(entry, 'ccy') + code = self.safe_currency_code(id) + result[code] = { + 'info': entry, + 'id': id, + 'code': code, + 'networks': None, + 'type': None, + 'name': None, + 'active': None, + 'deposit': None, + 'withdraw': None, + 'fee': None, + 'precision': None, + 'limits': { + 'amount': { + 'min': self.safe_number(entry, 'min'), + 'max': self.safe_number(entry, 'max'), + }, + 'withdraw': { + 'min': None, + 'max': None, + }, + 'deposit': { + 'min': None, + 'max': None, + }, + }, + 'created': None, + } + return result + + def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody): + if not response: + return None # fallback to default error handler + # + # { + # "code": "1", + # "data": [ + # { + # "clOrdId": "", + # "ordId": "", + # "sCode": "51119", + # "sMsg": "Order placement failed due to insufficient balance. ", + # "tag": "" + # } + # ], + # "msg": "" + # }, + # { + # "code": "58001", + # "data": [], + # "msg": "Incorrect trade password" + # } + # + code = self.safe_string(response, 'code') + if (code != '0') and (code != '2'): # 2 means that bulk operation partially succeeded + feedback = self.id + ' ' + body + data = self.safe_list(response, 'data', []) + for i in range(0, len(data)): + error = data[i] + errorCode = self.safe_string(error, 'sCode') + message = self.safe_string(error, 'sMsg') + self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback) + self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) + self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback) + raise ExchangeError(feedback) # unknown message + return None + + async def fetch_margin_adjustment_history(self, symbol: Str = None, type: Str = None, since: Num = None, limit: Num = None, params={}) -> List[MarginModification]: + """ + fetches the history of margin added or reduced from contract isolated positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-7-days + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-bills-details-last-3-months + + :param str [symbol]: not used by okx fetchMarginAdjustmentHistory + :param str [type]: "add" or "reduce" + :param int [since]: the earliest time in ms to fetch margin adjustment history for + :param int [limit]: the maximum number of entries to retrieve + :param dict params: extra parameters specific to the exchange api endpoint + :param boolean [params.auto]: True if fetching auto margin increases + :returns dict[]: a list of `margin structures ` + """ + await self.load_markets() + auto = self.safe_bool(params, 'auto') + if type is None: + raise ArgumentsRequired(self.id + ' fetchMarginAdjustmentHistory() requires a type argument') + isAdd = type == 'add' + subType = '160' if isAdd else '161' + if auto: + if isAdd: + subType = '162' + else: + raise BadRequest(self.id + ' cannot fetch margin adjustments for type ' + type) + request: dict = { + 'subType': subType, + 'mgnMode': 'isolated', + } + until = self.safe_integer(params, 'until') + params = self.omit(params, 'until') + if since is not None: + request['startTime'] = since + if limit is not None: + request['limit'] = limit + if until is not None: + request['endTime'] = until + response = None + now = self.milliseconds() + oneWeekAgo = now - 604800000 + threeMonthsAgo = now - 7776000000 + if (since is None) or (since > oneWeekAgo): + response = await self.privateGetAccountBills(self.extend(request, params)) + elif since > threeMonthsAgo: + response = await self.privateGetAccountBillsArchive(self.extend(request, params)) + else: + raise BadRequest(self.id + ' fetchMarginAdjustmentHistory() cannot fetch margin adjustments older than 3 months') + # + # { + # code: '0', + # data: [ + # { + # bal: '67621.4325135010619812', + # balChg: '-10.0000000000000000', + # billId: '691293628710342659', + # ccy: 'USDT', + # clOrdId: '', + # execType: '', + # fee: '0', + # fillFwdPx: '', + # fillIdxPx: '', + # fillMarkPx: '', + # fillMarkVol: '', + # fillPxUsd: '', + # fillPxVol: '', + # fillTime: '1711089244850', + # from: '', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # interest: '0', + # mgnMode: 'isolated', + # notes: '', + # ordId: '', + # pnl: '0', + # posBal: '73.12', + # posBalChg: '10.00', + # px: '', + # subType: '160', + # sz: '10', + # tag: '', + # to: '', + # tradeId: '0', + # ts: '1711089244699', + # type: '6' + # } + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + modifications = self.parse_margin_modifications(data) + return self.filter_by_symbol_since_limit(modifications, symbol, since, limit) + + async def fetch_positions_history(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: + """ + fetches historical positions + + https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions-history + + :param str [symbols]: unified market symbols + :param int [since]: timestamp in ms of the earliest position to fetch + :param int [limit]: the maximum amount of records to fetch, default=100, max=100 + :param dict params: extra parameters specific to the exchange api endpoint + :param str [params.marginMode]: "cross" or "isolated" + + EXCHANGE SPECIFIC PARAMETERS + :param str [params.instType]: margin, swap, futures or option + :param str [params.type]: the type of latest close position 1: close position partially, 2:close all, 3:liquidation, 4:partial liquidation; 5:adl, is it is the latest type if there are several types for the same position + :param str [params.posId]: position id, there is attribute expiration, the posid will be expired if it is more than 30 days after the last full close position, then position will use new posid + :param str [params.before]: timestamp in ms of the earliest position to fetch based on the last update time of the position + :param str [params.after]: timestamp in ms of the latest position to fetch based on the last update time of the position + :returns dict[]: a list of `position structures ` + """ + await self.load_markets() + marginMode = self.safe_string(params, 'marginMode') + instType = self.safe_string_upper(params, 'instType') + params = self.omit(params, ['until', 'marginMode', 'instType']) + if limit is None: + limit = 100 + request: dict = { + 'limit': limit, + } + if symbols is not None: + symbolsLength = len(symbols) + if symbolsLength == 1: + market = self.market(symbols[0]) + request['instId'] = market['id'] + if marginMode is not None: + request['mgnMode'] = marginMode + if instType is not None: + request['instType'] = instType + response = await self.privateGetAccountPositionsHistory(self.extend(request, params)) + # + # { + # code: '0', + # data: [ + # { + # cTime: '1708735940395', + # ccy: 'USDT', + # closeAvgPx: '0.6330444444444444', + # closeTotalPos: '27', + # direction: 'long', + # fee: '-1.69566', + # fundingFee: '-11.870404179341788', + # instId: 'XRP-USDT-SWAP', + # instType: 'SWAP', + # lever: '3.0', + # liqPenalty: '0', + # mgnMode: 'cross', + # openAvgPx: '0.623', + # openMaxPos: '15', + # pnl: '27.11999999999988', + # pnlRatio: '0.0241732402722634', + # posId: '681423155054862336', + # realizedPnl: '13.553935820658092', + # triggerPx: '', + # type: '2', + # uTime: '1711088748170', + # uly: 'XRP-USDT' + # }, + # ... + # ], + # msg: '' + # } + # + data = self.safe_list(response, 'data') + positions = self.parse_positions(data, symbols, params) + return self.filter_by_since_limit(positions, since, limit) + + async def fetch_long_short_ratio_history(self, symbol: Str = None, timeframe: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LongShortRatio]: + """ + fetches the long short ratio history for a unified market symbol + + https://www.okx.com/docs-v5/en/#trading-statistics-rest-api-get-contract-long-short-ratio + + :param str symbol: unified symbol of the market to fetch the long short ratio for + :param str [timeframe]: the period for the ratio + :param int [since]: the earliest time in ms to fetch ratios for + :param int [limit]: the maximum number of long short ratio structures to retrieve + :param dict [params]: extra parameters specific to the exchange API endpoint + :param int [params.until]: timestamp in ms of the latest ratio to fetch + :returns dict[]: an array of `long short ratio structures ` + """ + await self.load_markets() + market = self.market(symbol) + request: dict = { + 'instId': market['id'], + } + until = self.safe_string_2(params, 'until', 'end') + params = self.omit(params, 'until') + if until is not None: + request['end'] = until + if timeframe is not None: + request['period'] = timeframe + if since is not None: + request['begin'] = since + if limit is not None: + request['limit'] = limit + response = await self.publicGetRubikStatContractsLongShortAccountRatioContract(self.extend(request, params)) + # + # { + # "code": "0", + # "data": [ + # ["1729323600000", "0.9398602814619824"], + # ["1729323300000", "0.9398602814619824"], + # ["1729323000000", "0.9398602814619824"], + # ], + # "msg": "" + # } + # + data = self.safe_list(response, 'data', []) + result = [] + for i in range(0, len(data)): + entry = data[i] + result.append({ + 'timestamp': self.safe_string(entry, 0), + 'longShortRatio': self.safe_string(entry, 1), + }) + return self.parse_long_short_ratio_history(result, market) + + def parse_long_short_ratio(self, info: dict, market: Market = None) -> LongShortRatio: + timestamp = self.safe_integer(info, 'timestamp') + symbol = None + if market is not None: + symbol = market['symbol'] + return { + 'info': info, + 'symbol': symbol, + 'timestamp': timestamp, + 'datetime': self.iso8601(timestamp), + 'timeframe': None, + 'longShortRatio': self.safe_number(info, 'longShortRatio'), + } diff --git a/config_examples/config_freqai.okx.json b/config_examples/config_freqai.okx.json index 7c7f055..67f8d83 100644 --- a/config_examples/config_freqai.okx.json +++ b/config_examples/config_freqai.okx.json @@ -31,14 +31,17 @@ }, "ccxt_async_config": { "enableRateLimit": true, - "rateLimit": 1000, + "rateLimit": 500, "timeout": 20000 }, "pair_whitelist": [ "BTC/USDT", "SOL/USDT" ], - "pair_blacklist": [] + "pair_blacklist": [ + "LAYER/USD", + "LAYER/USDT" + ] }, "entry_pricing": { "price_side": "same", @@ -67,7 +70,7 @@ }, "freqaimodel": "CatboostClassifier", "purge_old_models": 2, - "identifier": "test130", + "identifier": "test131", "train_period_days": 30, "backtest_period_days": 10, "live_retrain_hours": 0, diff --git a/docker-compose.yml b/docker-compose.yml index 3689d3b..850fa61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: - "./config_examples:/freqtrade/config_examples" - "./freqtrade/templates:/freqtrade/templates" - "./freqtrade/exchange/:/freqtrade/exchange" + - "./ccxt/async_support/okx.py:/home/ftuser/.local/lib/python3.12/site-packages/ccxt/async_support/okx.py" # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation # for more information. diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 343c073..1d7ed33 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -11,9 +11,9 @@ logger = logging.getLogger(__name__) class FreqaiExampleStrategy(IStrategy): minimal_roi = { - "0": 0.076, - "7": 0.034, - "13": 0.007, + "0": 0.02, + "7": 0.01, + "13": 0.005, "60": 0 } @@ -24,29 +24,25 @@ class FreqaiExampleStrategy(IStrategy): startup_candle_count: int = 40 can_short = False - # Hyperopt 参数 buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=True, load=True) sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=True, load=True) roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True) roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True) roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True) stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True) - trailing_stop_positive_offset = DecimalParameter(low=0.01, high=0.5, default=0.02, space="trailing", optimize=True, load=True) + trailing_stop_positive_offset = DecimalParameter(low=0.005, high=0.5, default=0.01, space="trailing", optimize=True, load=True) - protections = [ - {"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120}, - {"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05} - ] + protections = [] freqai_info = { "model": "LightGBMRegressor", "feature_parameters": { "include_timeframes": ["5m"], - "include_corr_pairlist": ["SOL/USDT", "BTC/USDT"], + "include_corr_pairlist": ["SOL/USDT"], "label_period_candles": 12, "include_shifted_candles": 0, - "include_periods": [10, 20], - "DI_threshold": 3.0 + "include_periods": [20], + "DI_threshold": 5.0 }, "data_split_parameters": { "test_size": 0.2, @@ -62,14 +58,20 @@ class FreqaiExampleStrategy(IStrategy): } plot_config = { - "main_plot": {}, + "main_plot": { + "close": {"color": "blue"}, + "bb_lowerband": {"color": "purple"} + }, "subplots": { "&-buy_rsi": {"&-buy_rsi": {"color": "green"}}, "&-sell_rsi": {"&-sell_rsi": {"color": "red"}}, - "&-stoploss": {"&-stoploss": {"color": "purple"}}, - "&-roi_0": {"&-roi_0": {"color": "orange"}}, + "rsi": {"rsi": {"color": "black"}}, "do_predict": {"do_predict": {"color": "brown"}}, - }, + "trade_signals": { + "enter_long": {"color": "green", "type": "scatter"}, + "exit_long": {"color": "red", "type": "scatter"} + } + } } def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: @@ -130,12 +132,10 @@ class FreqaiExampleStrategy(IStrategy): logger.info(f"DataFrame rows: {len(dataframe)}") logger.info(f"Columns before freqai.start: {list(dataframe.columns)}") - # 验证输入数据 if "close" not in dataframe.columns or dataframe["close"].isna().all(): logger.error(f"DataFrame missing 'close' column or all NaN for pair: {metadata['pair']}") raise ValueError("DataFrame missing valid 'close' column") - # 生成 RSI if len(dataframe) < 14: logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute rsi") dataframe["rsi"] = 50 @@ -143,7 +143,6 @@ class FreqaiExampleStrategy(IStrategy): dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) logger.info(f"rsi stats: {dataframe['rsi'].describe().to_string()}") - # 生成 %-volatility if len(dataframe) < 20 or dataframe["close"].isna().any(): logger.warning(f"DataFrame too short ({len(dataframe)} rows) or contains NaN in close, cannot compute %-volatility") dataframe["%-volatility"] = 0 @@ -154,7 +153,6 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-volatility"] = (dataframe["%-volatility"] - dataframe["%-volatility"].mean()) / dataframe["%-volatility"].std() logger.info(f"%-volatility stats: {dataframe['%-volatility'].describe().to_string()}") - # 生成 TEMA if len(dataframe) < 9: logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute tema") dataframe["tema"] = dataframe["close"] @@ -165,7 +163,6 @@ class FreqaiExampleStrategy(IStrategy): dataframe["tema"] = dataframe["tema"].fillna(dataframe["close"]) logger.info(f"tema stats: {dataframe['tema'].describe().to_string()}") - # 生成 Bollinger Bands if len(dataframe) < 20: logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute bb_lowerband") dataframe["bb_lowerband"] = dataframe["close"] @@ -177,21 +174,6 @@ class FreqaiExampleStrategy(IStrategy): dataframe["bb_lowerband"] = dataframe["bb_lowerband"].fillna(dataframe["close"]) logger.info(f"bb_lowerband stats: {dataframe['bb_lowerband'].describe().to_string()}") - # 生成 up_or_down - label_period = self.freqai_info["feature_parameters"]["label_period_candles"] - if len(dataframe) < label_period + 1: - logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute up_or_down") - dataframe["up_or_down"] = 0 - else: - dataframe["up_or_down"] = np.where( - dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0 - ) - if dataframe["up_or_down"].isna().any(): - logger.warning("up_or_down contains NaN, filling with 0") - dataframe["up_or_down"] = dataframe["up_or_down"].fillna(0) - logger.info(f"up_or_down stats: {dataframe['up_or_down'].describe().to_string()}") - - # 生成其他特征 if "date" in dataframe.columns: dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour @@ -200,7 +182,6 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-day_of_week"] = 0 dataframe["%-hour_of_day"] = 0 - # 调用 FreqAI try: dataframe = self.freqai.start(dataframe, metadata, self) logger.info(f"Columns after freqai.start: {list(dataframe.columns)}") @@ -210,26 +191,23 @@ class FreqaiExampleStrategy(IStrategy): dataframe["sell_rsi_pred"] = 80 dataframe["do_predict"] = 1 - # 检查预测列 for col in ["buy_rsi_pred", "sell_rsi_pred"]: if col not in dataframe.columns: logger.error(f"Error: {col} column not generated for pair: {metadata['pair']}") dataframe[col] = 50 if col == "buy_rsi_pred" else 80 logger.info(f"{col} stats: {dataframe[col].describe().to_string()}") - # 调试特征分布 - if "%-bb_width-period_10_SOL/USDT_5m" in dataframe.columns: - if dataframe["%-bb_width-period_10_SOL/USDT_5m"].std() > 0: - dataframe["%-bb_width-period_10_SOL/USDT_5m"] = ( - dataframe["%-bb_width-period_10_SOL/USDT_5m"] - dataframe["%-bb_width-period_10_SOL/USDT_5m"].mean() - ) / dataframe["%-bb_width-period_10_SOL/USDT_5m"].std() - logger.info(f"%-bb_width-period_10 stats: {dataframe['%-bb_width-period_10_SOL/USDT_5m'].describe().to_string()}") + if "%-bb_width-period_20_SOL/USDT_5m" in dataframe.columns: + if dataframe["%-bb_width-period_20_SOL/USDT_5m"].std() > 0: + dataframe["%-bb_width-period_20_SOL/USDT_5m"] = ( + dataframe["%-bb_width-period_20_SOL/USDT_5m"] - dataframe["%-bb_width-period_20_SOL/USDT_5m"].mean() + ) / dataframe["%-bb_width-period_20_SOL/USDT_5m"].std() + logger.info(f"%-bb_width-period_20 stats: {dataframe['%-bb_width-period_20_SOL/USDT_5m'].describe().to_string()}") - # 动态生成期望的特征列 def get_expected_columns(freqai_config: dict) -> list: indicators = ["rsi", "bb_width", "pct-change"] - periods = freqai_config.get("feature_parameters", {}).get("include_periods", [10, 20]) - pairs = freqai_config.get("include_corr_pairlist", ["SOL/USDT", "BTC/USDT"]) + periods = freqai_config.get("feature_parameters", {}).get("include_periods", [20]) + pairs = freqai_config.get("include_corr_pairlist", ["SOL/USDT"]) timeframes = freqai_config.get("include_timeframes", ["5m"]) shifts = [0] expected_columns = ["%-volatility", "%-day_of_week", "%-hour_of_day"] @@ -248,50 +226,47 @@ class FreqaiExampleStrategy(IStrategy): expected_columns = get_expected_columns(self.freqai_info) logger.info(f"Expected feature columns ({len(expected_columns)}): {expected_columns[:10]}...") - # 比较特征集 actual_columns = list(dataframe.columns) missing_columns = [col for col in expected_columns if col not in actual_columns] extra_columns = [col for col in actual_columns if col not in expected_columns and col.startswith("%-")] logger.info(f"Missing columns ({len(missing_columns)}): {missing_columns}") logger.info(f"Extra columns ({len(extra_columns)}): {extra_columns}") - # 调试 DI 丢弃预测 if "DI_values" in dataframe.columns: logger.info(f"DI_values stats: {dataframe['DI_values'].describe().to_string()}") logger.info(f"DI discarded predictions: {len(dataframe[dataframe['do_predict'] == 0])}") - # 清理数据 dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0) logger.info(f"Final columns in populate_indicators: {list(dataframe.columns)}") return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: enter_long_conditions = [ - qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"] + (5 if metadata["pair"] == "BTC/USDT" else 0)), + qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), df["tema"] > df["tema"].shift(1), df["volume"] > 0, - df["do_predict"] == 1, - df["up_or_down"] == 1 + df["do_predict"] == 1 ] - if enter_long_conditions: - df.loc[ - reduce(lambda x, y: x & y, enter_long_conditions), - ["enter_long", "enter_tag"] - ] = (1, "long") + df["entry_signal"] = reduce(lambda x, y: x & y, enter_long_conditions) + df["entry_signal"] = df["entry_signal"].rolling(window=2, min_periods=1).max().astype(bool) + df.loc[ + df["entry_signal"], + ["enter_long", "enter_tag"] + ] = (1, "long") + if df["entry_signal"].iloc[-1]: + logger.info(f"Entry signal triggered for {metadata['pair']}: rsi={df['rsi'].iloc[-1]}, buy_rsi_pred={df['buy_rsi_pred'].iloc[-1]}, do_predict={df['do_predict'].iloc[-1]}") return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: exit_long_conditions = [ - (qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"])) | + (qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"] - 5)) | (df["close"] < df["close"].shift(1) * 0.98) | (df["close"] < df["bb_lowerband"]), df["volume"] > 0, - df["do_predict"] == 1, - df["up_or_down"] == 0 + df["do_predict"] == 1 ] - time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=1)) df.loc[ - (reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit, + reduce(lambda x, y: x & y, exit_long_conditions), "exit_long" ] = 1 return df @@ -300,9 +275,16 @@ class FreqaiExampleStrategy(IStrategy): self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time, entry_tag, side: str, **kwargs ) -> bool: + logger.info(f"Confirming trade entry for {pair}, order_type: {order_type}, rate: {rate}, current_time: {current_time}") df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = df.iloc[-1].squeeze() if side == "long": - if rate > (last_candle["close"] * (1 + 0.001)): - return False + if order_type == "market": + logger.info(f"Order confirmed for {pair}, rate: {rate} (market order)") + return True + if rate <= (last_candle["close"] * (1 + 0.01)): + logger.info(f"Order confirmed for {pair}, rate: {rate}") + return True + logger.info(f"Order rejected: rate {rate} exceeds threshold {last_candle['close'] * 1.01}") + return False return True