From 6fdcf3a10a28d8b6fa426b06d0972ead3d8ae7e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 May 2022 10:56:13 +0200 Subject: [PATCH 1/3] Support both position modes on OKX --- docs/exchanges.md | 6 ++- freqtrade/exchange/exchange.py | 12 +++++- freqtrade/exchange/kraken.py | 3 ++ freqtrade/exchange/okx.py | 37 +++++++++++++++++- tests/exchange/test_exchange.py | 8 ++++ tests/exchange/test_okx.py | 66 +++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 18a7af5a1..b2759893b 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -228,7 +228,11 @@ OKX requires a passphrase for each api key, you will therefore need to add this ``` !!! Warning - OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. + OKX only provides 300 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. + +!!! Warning "Futures - position mode" + OKX Futures has the concept of "position mode" - which can be Net or long/short (hedge mode). + Freqtrade supports both modes - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades. ## Gate.io diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 59089b630..ce2c06ae0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -198,6 +198,7 @@ class Exchange: if self.trading_mode != TradingMode.SPOT: self.fill_leverage_tiers() + self.additional_exchange_init() def __del__(self): """ @@ -294,6 +295,14 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode + 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. + """ + pass + def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -944,6 +953,7 @@ class Exchange: def _get_params( self, + side: BuySell, ordertype: str, leverage: float, reduceOnly: bool, @@ -973,7 +983,7 @@ class Exchange: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order - params = self._get_params(ordertype, leverage, reduceOnly, time_in_force) + params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 94727afa6..ea9b73fab 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple import ccxt from pandas import DataFrame +from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -165,12 +166,14 @@ class Kraken(Exchange): def _get_params( self, + side: BuySell, ordertype: str, leverage: float, reduceOnly: bool, time_in_force: str = 'gtc' ) -> Dict: params = super()._get_params( + side=side, ordertype=ordertype, leverage=leverage, reduceOnly=reduceOnly, diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 3599d334b..56636bf21 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -35,14 +35,48 @@ class Okx(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED), ] + net_only = True + + @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: + accounts = self._api.fetch_accounts() + 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( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + 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' + def _get_params( self, + side: BuySell, ordertype: str, leverage: float, reduceOnly: bool, time_in_force: str = 'gtc', ) -> Dict: params = super()._get_params( + side=side, ordertype=ordertype, leverage=leverage, reduceOnly=reduceOnly, @@ -50,6 +84,7 @@ class Okx(Exchange): ) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: params['tdMode'] = self.margin_mode.value + params['posSide'] = self._get_posSide(side, reduceOnly) return params @retrier @@ -62,7 +97,7 @@ class Okx(Exchange): symbol=pair, params={ "mgnMode": self.margin_mode.value, - # "posSide": "net"", + "posSide": self._get_posSide(side, False), }) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1368bcb85..77a04ac6c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -99,6 +99,8 @@ def test_remove_credentials(default_conf, caplog) -> None: def test_init_ccxt_kwargs(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + aei_mock = mocker.patch('freqtrade.exchange.Exchange.additional_exchange_init') + caplog.set_level(logging.INFO) conf = copy.deepcopy(default_conf) conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True} @@ -108,6 +110,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): caplog) assert ex._api_async.aiohttp_trust_env assert not ex._api.aiohttp_trust_env + assert aei_mock.call_count == 1 # Reset logging and config caplog.clear() @@ -4758,8 +4761,10 @@ def test__get_params(mocker, default_conf, exchange_name): if exchange_name == 'okx': params2['tdMode'] = 'isolated' + params2['posSide'] = 'net' assert exchange._get_params( + side="buy", ordertype='market', reduceOnly=False, time_in_force='gtc', @@ -4767,6 +4772,7 @@ def test__get_params(mocker, default_conf, exchange_name): ) == params1 assert exchange._get_params( + side="buy", ordertype='market', reduceOnly=False, time_in_force='ioc', @@ -4774,6 +4780,7 @@ def test__get_params(mocker, default_conf, exchange_name): ) == params1 assert exchange._get_params( + side="buy", ordertype='limit', reduceOnly=False, time_in_force='gtc', @@ -4786,6 +4793,7 @@ def test__get_params(mocker, default_conf, exchange_name): exchange._params = {'test': True} assert exchange._get_params( + side="buy", ordertype='limit', reduceOnly=True, time_in_force='ioc', diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 37c1ea974..8981b1a38 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,7 +1,10 @@ from unittest.mock import MagicMock, PropertyMock +import pytest + from freqtrade.enums import MarginMode, TradingMode from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers def test_get_maintenance_ratio_and_amt_okx( @@ -170,6 +173,69 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers +@pytest.mark.parametrize('mode,side,reduceonly,result', [ + ('net', 'buy', False, 'net'), + ('net', 'sell', True, 'net'), + ('net', 'sell', False, 'net'), + ('net', 'buy', True, 'net'), + ('longshort', 'buy', False, 'long'), + ('longshort', 'sell', True, 'long'), + ('longshort', 'sell', False, 'short'), + ('longshort', 'buy', True, 'short'), +]) +def test__get_posSide(default_conf, mocker, mode, side, reduceonly, result): + + exchange = get_patched_exchange(mocker, default_conf, id="okx") + exchange.net_only = mode == 'net' + assert exchange._get_posSide(side, reduceonly) == result + + +def test_additional_exchange_init_okx(default_conf, mocker): + api_mock = MagicMock() + api_mock.fetch_accounts = MagicMock(return_value=[ + {'id': '2555', + 'type': '2', + 'currency': None, + 'info': {'acctLv': '2', + 'autoLoan': False, + 'ctIsoMode': 'automatic', + 'greeksType': 'PA', + 'level': 'Lv1', + 'levelTmp': '', + 'mgnIsoMode': 'automatic', + 'posMode': 'long_short_mode', + 'uid': '2555'}}]) + exchange = get_patched_exchange(mocker, default_conf, id="okx", api_mock=api_mock) + assert api_mock.fetch_accounts.call_count == 0 + exchange.trading_mode = TradingMode.FUTURES + # Default to netOnly + assert exchange.net_only + exchange.additional_exchange_init() + assert api_mock.fetch_accounts.call_count == 1 + assert not exchange.net_only + + api_mock.fetch_accounts = MagicMock(return_value=[ + {'id': '2555', + 'type': '2', + 'currency': None, + 'info': {'acctLv': '2', + 'autoLoan': False, + 'ctIsoMode': 'automatic', + 'greeksType': 'PA', + 'level': 'Lv1', + 'levelTmp': '', + 'mgnIsoMode': 'automatic', + 'posMode': 'net_mode', + 'uid': '2555'}}]) + exchange.additional_exchange_init() + assert api_mock.fetch_accounts.call_count == 1 + assert exchange.net_only + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'okx', + "additional_exchange_init", "fetch_accounts") + + def test_load_leverage_tiers_okx(default_conf, mocker, markets): api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={ From 149704e748272f34dd541c2335ce76e9a7e12dac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 May 2022 11:08:54 +0200 Subject: [PATCH 2/3] Fix wrong type --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/exchange/ftx.py | 3 ++- freqtrade/exchange/kraken.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ce2c06ae0..65b9fb628 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -946,7 +946,7 @@ class Exchange: # Order handling - def _lev_prep(self, pair: str, leverage: float, side: str): + def _lev_prep(self, pair: str, leverage: float, side: BuySell): if self.trading_mode != TradingMode.SPOT: self.set_margin_mode(pair, self.margin_mode) self._set_leverage(leverage, pair) @@ -1068,7 +1068,7 @@ class Exchange: @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, - side: str, leverage: float) -> Dict: + side: BuySell, leverage: float) -> Dict: """ creates a stoploss order. requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index d2dcf84a6..65c2a53ca 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Tuple import ccxt +from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) @@ -44,7 +45,7 @@ class Ftx(Exchange): @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, - order_types: Dict, side: str, leverage: float) -> Dict: + order_types: Dict, side: BuySell, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index ea9b73fab..33a2c7f87 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -96,7 +96,7 @@ class Kraken(Exchange): @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, - order_types: Dict, side: str, leverage: float) -> Dict: + order_types: Dict, side: BuySell, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. From dc0c1bf87dcd597f2f33614e35c4f43b71bc0efc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 May 2022 13:13:26 +0200 Subject: [PATCH 3/3] Only fetch accounts when authenticated. --- freqtrade/exchange/okx.py | 2 +- tests/exchange/test_ccxt_compat.py | 1 + tests/exchange/test_okx.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 56636bf21..9aeefd450 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -45,7 +45,7 @@ class Okx(Exchange): Must be overridden in child methods if required. """ try: - if self.trading_mode == TradingMode.FUTURES: + if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: accounts = self._api.fetch_accounts() if len(accounts) > 0: self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode' diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 2a148c388..d8832bb71 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -135,6 +135,7 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch( 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') + class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) yield exchange, request.param diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 8981b1a38..f6bdd35ad 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -205,6 +205,7 @@ def test_additional_exchange_init_okx(default_conf, mocker): 'mgnIsoMode': 'automatic', 'posMode': 'long_short_mode', 'uid': '2555'}}]) + default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, id="okx", api_mock=api_mock) assert api_mock.fetch_accounts.call_count == 0 exchange.trading_mode = TradingMode.FUTURES