stable/freqtrade/exchange/okx.py

239 lines
9.0 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
2022-09-09 18:31:30 +00:00
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
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,
2021-11-02 18:49:53 +00:00
}
_ft_has_futures: Dict = {
2022-03-18 15:49:37 +00:00
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
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
@retrier
2022-05-07 06:45:37 +00:00
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
2022-03-23 05:49:17 +00:00
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
2022-02-16 08:02:11 +00:00
# TODO-lev: Test me properly (check mgnMode passed)
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:
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:
2022-11-09 19:17:10 +00:00
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopLossPrice': 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 stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
OKX uses non-default stoploss price naming.
"""
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
return (
order.get('stopLossPrice', None) is None
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
)
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 order_reg
except ccxt.OrderNotFound:
pass
2022-11-09 19:17:10 +00:00
params1 = {'stop': True, 'ordType': 'conditional'}
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
self._api.fetch_canceled_orders):
try:
orders = method(pair, params=params1)
orders_f = [order for order in orders if order['id'] == order_id]
if orders_f:
order = orders_f[0]
if (order['status'] == 'closed'
2022-11-08 19:24:26 +00:00
and (real_order_id := order.get('info', {}).get('ordId')) is not None):
# Once a order triggered, we fetch the regular followup order.
2022-11-08 19:24:26 +00:00
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
2023-03-19 19:03:34 +00:00
order_reg['type'] = 'stoploss'
2022-11-08 19:24:26 +00:00
order_reg['status_stop'] = 'triggered'
return order_reg
order['type'] = 'stoploss'
return 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,
)