freqtrade_origin/freqtrade/exchange/okx.py

243 lines
9.2 KiB
Python
Raw Normal View History

2021-11-02 18:49:53 +00:00
import logging
2022-11-08 19:24:26 +00:00
from typing import Any, Dict, List, Optional, Tuple
2021-11-02 18:49:53 +00:00
import ccxt
2022-05-07 06:45:37 +00:00
from freqtrade.constants import BuySell
2023-09-10 16:10:38 +00:00
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
TemporaryError)
from freqtrade.exchange import Exchange, date_minus_candles
2022-02-15 06:04:50 +00:00
from freqtrade.exchange.common import retrier
2022-11-08 19:24:26 +00:00
from freqtrade.misc import safe_value_fallback2
2021-11-02 18:49:53 +00:00
logger = logging.getLogger(__name__)
2022-02-08 18:45:39 +00:00
class Okx(Exchange):
"""Okx exchange class.
2021-11-09 10:31:54 +00:00
Contains adjustments needed for Freqtrade to work with this exchange.
2021-11-02 18:49:53 +00:00
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
2021-12-19 13:48:59 +00:00
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
"stop_price_param": "stopLossPrice",
2021-11-02 18:49:53 +00:00
}
_ft_has_futures: Dict = {
2022-03-18 15:49:37 +00:00
"tickers_have_quoteVolume": False,
2023-03-20 08:00:00 +00:00
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
PriceType.INDEX: "mark",
},
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
2022-05-07 08:56:13 +00:00
net_only = True
2022-08-22 18:23:19 +00:00
_ccxt_params: Dict = {'options': {'brokerId': 'ffb5405ad327SUDE'}}
2022-05-14 07:51:44 +00:00
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
2022-05-14 07:51:44 +00:00
"""
Exchange ohlcv candle limit
OKX has the following behaviour:
* 300 candles for uptodate data
* 100 candles for historic data
* 100 candles for additional candles (not futures or spot).
2022-05-14 07:51:44 +00:00
:param timeframe: Timeframe to check
:param candle_type: Candle-type
2022-05-15 15:06:40 +00:00
:param since_ms: Starting timestamp
2022-05-14 07:51:44 +00:00
:return: Candle limit as integer
"""
if (
candle_type in (CandleType.FUTURES, CandleType.SPOT) and
(not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
):
return 300
2022-05-14 07:51:44 +00:00
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
2022-05-14 07:51:44 +00:00
2022-05-07 08:56:13 +00:00
@retrier
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Must be overridden in child methods if required.
"""
try:
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
2022-05-07 08:56:13 +00:00
accounts = self._api.fetch_accounts()
2022-09-21 18:59:12 +00:00
self._log_exchange_response('fetch_accounts', accounts)
2022-05-07 08:56:13 +00:00
if len(accounts) > 0:
self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode'
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
2022-10-01 07:32:16 +00:00
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
) from e
2022-05-07 08:56:13 +00:00
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _get_posSide(self, side: BuySell, reduceOnly: bool):
if self.net_only:
return 'net'
if not reduceOnly:
# Enter
return 'long' if side == 'buy' else 'short'
else:
# Exit
return 'long' if side == 'sell' else 'short'
2022-02-16 08:02:11 +00:00
def _get_params(
self,
2022-05-07 08:56:13 +00:00
side: BuySell,
2022-02-16 08:02:11 +00:00
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
2022-02-16 08:02:11 +00:00
) -> Dict:
params = super()._get_params(
2022-05-07 08:56:13 +00:00
side=side,
2022-02-16 08:02:11 +00:00
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
2022-05-07 08:56:13 +00:00
params['posSide'] = self._get_posSide(side, reduceOnly)
2022-02-16 08:02:11 +00:00
return params
def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
try:
res_lev = self._api.fetch_leverage(symbol=pair, params={
"mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False),
})
self._log_exchange_response('get_leverage', res_lev)
already_set = all(float(x['lever']) == leverage for x in res_lev['data'])
return already_set
except ccxt.BaseError:
# Assume all errors as "not set yet"
return False
@retrier
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
2022-03-23 05:49:17 +00:00
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
2023-02-07 19:37:06 +00:00
res = self._api.set_leverage(
leverage=leverage,
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
2022-05-07 08:56:13 +00:00
"posSide": self._get_posSide(side, False),
})
2023-02-07 19:37:06 +00:00
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
already_set = self.__fetch_leverage_already_set(pair, leverage, side)
if not already_set:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}'
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
2022-02-07 09:44:37 +00:00
def get_max_pair_stake_amount(
self,
pair: str,
price: float,
leverage: float = 1.0
) -> float:
if self.trading_mode == TradingMode.SPOT:
return float('inf') # Not actually inf, but this probably won't matter for SPOT
if pair not in self._leverage_tiers:
return float('inf')
2022-02-07 09:44:37 +00:00
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['maxNotional'] / leverage
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = super()._get_stop_params(side, ordertype, stop_price)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, True)
return params
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
if (
order['status'] == 'closed'
and (real_order_id := order.get('info', {}).get('ordId')) is not None
):
# Once a order triggered, we fetch the regular followup order.
order_reg = self.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order_reg)
order_reg['id_stop'] = order_reg['id']
order_reg['id'] = order_id
order_reg['type'] = 'stoploss'
order_reg['status_stop'] = 'triggered'
return order_reg
order = self._order_contracts_to_amount(order)
order['type'] = 'stoploss'
return order
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
params1 = {'stop': True}
order_reg = self._api.fetch_order(order_id, pair, params=params1)
2023-03-20 08:00:00 +00:00
self._log_exchange_response('fetch_stoploss_order', order_reg)
return self._convert_stop_order(pair, order_id, order_reg)
except ccxt.OrderNotFound:
pass
2023-03-20 17:19:17 +00:00
params2 = {'stop': True, 'ordType': 'conditional'}
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
self._api.fetch_canceled_orders):
try:
2023-03-20 17:19:17 +00:00
orders = method(pair, params=params2)
orders_f = [order for order in orders if order['id'] == order_id]
if orders_f:
order = orders_f[0]
return self._convert_stop_order(pair, order_id, order)
except ccxt.BaseError:
pass
raise RetryableOrderError(
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
2022-11-08 19:24:26 +00:00
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
params1 = {'stop': True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)