freqtrade_origin/freqtrade/exchange/okx.py

289 lines
11 KiB
Python
Raw Normal View History

2021-11-02 18:49:53 +00:00
import logging
from datetime import timedelta
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
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
2022-11-08 19:24:26 +00:00
from freqtrade.misc import safe_value_fallback2
from freqtrade.util import dt_now, dt_ts
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
"""
2024-09-04 05:15:17 +00:00
_ft_has: FtHas = {
"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,
2024-06-20 16:24:43 +00:00
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
"ws_enabled": True,
2021-11-02 18:49:53 +00:00
}
2024-09-04 05:15:17 +00:00
_ft_has_futures: FtHas = {
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",
2024-05-12 14:58:33 +00:00
},
"ws_enabled": True,
}
_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
_ccxt_params: dict = {"options": {"brokerId": "ffb5405ad327SUDE"}}
2022-08-22 18:23:19 +00:00
2022-05-14 07:51:44 +00:00
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
2024-05-12 14:58:33 +00:00
) -> int:
2022-05-14 07:51:44 +00:00
"""
Exchange ohlcv candle limit
OKX has the following behaviour:
2024-04-18 20:51:25 +00:00
* 300 candles for up-to-date 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
"""
2024-05-12 14:58:33 +00:00
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:
2024-05-12 14:58:33 +00:00
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()
2024-05-12 14:58:33 +00:00
self._log_exchange_response("fetch_accounts", accounts)
2022-05-07 08:56:13 +00:00
if len(accounts) > 0:
2024-05-12 14:58:33 +00:00
self.net_only = accounts[0].get("info", {}).get("posMode") == "net_mode"
2022-05-07 08:56:13 +00:00
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
2022-05-07 08:56:13 +00:00
raise TemporaryError(
2024-05-12 14:58:33 +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:
2024-05-12 14:58:33 +00:00
return "net"
2022-05-07 08:56:13 +00:00
if not reduceOnly:
# Enter
2024-05-12 14:58:33 +00:00
return "long" if side == "buy" else "short"
2022-05-07 08:56:13 +00:00
else:
# Exit
2024-05-12 14:58:33 +00:00
return "long" if side == "sell" else "short"
2022-05-07 08:56:13 +00:00
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,
2024-05-12 14:58:33 +00:00
time_in_force: str = "GTC",
) -> dict:
2022-02-16 08:02:11 +00:00
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:
2024-05-12 14:58:33 +00:00
params["tdMode"] = self.margin_mode.value
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:
2024-05-12 14:58:33 +00:00
res_lev = self._api.fetch_leverage(
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False),
2024-05-12 14:58:33 +00:00
},
)
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),
2024-05-12 14:58:33 +00:00
},
)
self._log_exchange_response("set_leverage", res)
2023-02-07 19:37:06 +00:00
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
already_set = self.__fetch_leverage_already_set(pair, leverage, side)
if not already_set:
raise TemporaryError(
2024-05-12 14:58:33 +00:00
f"Could not set leverage due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
2024-05-12 14:58:33 +00:00
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
2022-02-07 09:44:37 +00:00
if self.trading_mode == TradingMode.SPOT:
2024-05-12 14:58:33 +00:00
return float("inf") # Not actually inf, but this probably won't matter for SPOT
2022-02-07 09:44:37 +00:00
if pair not in self._leverage_tiers:
2024-05-12 14:58:33 +00:00
return float("inf")
2022-02-07 09:44:37 +00:00
pair_tiers = self._leverage_tiers[pair]
2024-05-12 14:58:33 +00:00
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:
2024-05-12 14:58:33 +00:00
params["tdMode"] = self.margin_mode.value
params["posSide"] = self._get_posSide(side, True)
return params
2024-11-04 18:23:17 +00:00
def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
if (
2024-05-12 14:58:33 +00:00
order.get("status", "open") == "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)
2024-05-12 14:58:33 +00:00
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)
2024-05-12 14:58:33 +00:00
order["type"] = "stoploss"
return order
@retrier(retries=API_RETRY_COUNT)
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
2024-05-12 14:58:33 +00:00
if self._config["dry_run"]:
return self.fetch_dry_run_order(order_id)
try:
2024-05-12 14:58:33 +00:00
params1 = {"stop": True}
order_reg = self._api.fetch_order(order_id, pair, params=params1)
2024-05-12 14:58:33 +00:00
self._log_exchange_response("fetch_stoploss_order", order_reg)
return self._convert_stop_order(pair, order_id, order_reg)
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
return self._fetch_stop_order_fallback(order_id, pair)
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
2024-05-12 14:58:33 +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)
2024-05-12 14:58:33 +00:00
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.OrderNotFound, ccxt.InvalidOrder):
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
2024-05-12 14:58:33 +00:00
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
2024-11-04 18:23:17 +00:00
def get_order_id_conditional(self, order: CcxtOrder) -> str:
2024-05-12 14:58:33 +00:00
if order.get("type", "") == "stop":
return safe_value_fallback2(order, order, "id_stop", "id")
return order["id"]
2022-11-08 19:24:26 +00:00
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
2024-05-12 14:58:33 +00:00
params1 = {"stop": True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = []
orders = self._api.fetch_closed_orders(pair, since=since_ms)
2024-05-12 14:58:33 +00:00
if since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23)):
# Regular fetch_closed_orders only returns 7 days of data.
# Force usage of "archive" endpoint, which returns 3 months of data.
2024-05-12 14:58:33 +00:00
params = {"method": "privateGetTradeOrdersHistoryArchive"}
orders_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params)
orders.extend(orders_hist)
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders