Merge pull request #6789 from freqtrade/okx_positionmode

Okx positionmode
This commit is contained in:
Matthias 2022-05-07 14:30:44 +02:00 committed by GitHub
commit 26648e54cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 7 deletions

View File

@ -228,7 +228,11 @@ OKX requires a passphrase for each api key, you will therefore need to add this
``` ```
!!! Warning !!! 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 ## Gate.io

View File

@ -198,6 +198,7 @@ class Exchange:
if self.trading_mode != TradingMode.SPOT: if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_tiers() self.fill_leverage_tiers()
self.additional_exchange_init()
def __del__(self): def __del__(self):
""" """
@ -294,6 +295,14 @@ class Exchange:
"""exchange ccxt precisionMode""" """exchange ccxt precisionMode"""
return self._api.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: def _log_exchange_response(self, endpoint, response) -> None:
""" Log exchange responses """ """ Log exchange responses """
if self.log_responses: if self.log_responses:
@ -937,13 +946,14 @@ class Exchange:
# Order handling # 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: if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.margin_mode) self.set_margin_mode(pair, self.margin_mode)
self._set_leverage(leverage, pair) self._set_leverage(leverage, pair)
def _get_params( def _get_params(
self, self,
side: BuySell,
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
@ -973,7 +983,7 @@ class Exchange:
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage)
return dry_order 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: try:
# Set the precision for amount and price(rate) as accepted by the exchange # Set the precision for amount and price(rate) as accepted by the exchange
@ -1058,7 +1068,7 @@ class Exchange:
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, 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. creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market

View File

@ -4,6 +4,7 @@ from typing import Any, Dict, List, Tuple
import ccxt import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
@ -44,7 +45,7 @@ class Ftx(Exchange):
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, 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. Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order. depending on order_types.stoploss configuration, uses 'market' or limit order.

View File

@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
@ -95,7 +96,7 @@ class Kraken(Exchange):
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, 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. Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken. Stoploss market orders is the only stoploss type supported by kraken.
@ -165,12 +166,14 @@ class Kraken(Exchange):
def _get_params( def _get_params(
self, self,
side: BuySell,
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'gtc' time_in_force: str = 'gtc'
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side,
ordertype=ordertype, ordertype=ordertype,
leverage=leverage, leverage=leverage,
reduceOnly=reduceOnly, reduceOnly=reduceOnly,

View File

@ -35,14 +35,48 @@ class Okx(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED), (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 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'
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( def _get_params(
self, self,
side: BuySell,
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'gtc', time_in_force: str = 'gtc',
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side,
ordertype=ordertype, ordertype=ordertype,
leverage=leverage, leverage=leverage,
reduceOnly=reduceOnly, reduceOnly=reduceOnly,
@ -50,6 +84,7 @@ class Okx(Exchange):
) )
if self.trading_mode == TradingMode.FUTURES and self.margin_mode: if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, reduceOnly)
return params return params
@retrier @retrier
@ -62,7 +97,7 @@ class Okx(Exchange):
symbol=pair, symbol=pair,
params={ params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
# "posSide": "net"", "posSide": self._get_posSide(side, False),
}) })
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e

View File

@ -135,6 +135,7 @@ def exchange_futures(request, exchange_conf, class_mocker):
class_mocker.patch( class_mocker.patch(
'freqtrade.exchange.binance.Binance.fill_leverage_tiers') 'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') 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) exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
yield exchange, request.param yield exchange, request.param

View File

@ -99,6 +99,8 @@ def test_remove_credentials(default_conf, caplog) -> None:
def test_init_ccxt_kwargs(default_conf, mocker, caplog): def test_init_ccxt_kwargs(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
aei_mock = mocker.patch('freqtrade.exchange.Exchange.additional_exchange_init')
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True} 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) caplog)
assert ex._api_async.aiohttp_trust_env assert ex._api_async.aiohttp_trust_env
assert not ex._api.aiohttp_trust_env assert not ex._api.aiohttp_trust_env
assert aei_mock.call_count == 1
# Reset logging and config # Reset logging and config
caplog.clear() caplog.clear()
@ -4758,8 +4761,10 @@ def test__get_params(mocker, default_conf, exchange_name):
if exchange_name == 'okx': if exchange_name == 'okx':
params2['tdMode'] = 'isolated' params2['tdMode'] = 'isolated'
params2['posSide'] = 'net'
assert exchange._get_params( assert exchange._get_params(
side="buy",
ordertype='market', ordertype='market',
reduceOnly=False, reduceOnly=False,
time_in_force='gtc', time_in_force='gtc',
@ -4767,6 +4772,7 @@ def test__get_params(mocker, default_conf, exchange_name):
) == params1 ) == params1
assert exchange._get_params( assert exchange._get_params(
side="buy",
ordertype='market', ordertype='market',
reduceOnly=False, reduceOnly=False,
time_in_force='ioc', time_in_force='ioc',
@ -4774,6 +4780,7 @@ def test__get_params(mocker, default_conf, exchange_name):
) == params1 ) == params1
assert exchange._get_params( assert exchange._get_params(
side="buy",
ordertype='limit', ordertype='limit',
reduceOnly=False, reduceOnly=False,
time_in_force='gtc', time_in_force='gtc',
@ -4786,6 +4793,7 @@ def test__get_params(mocker, default_conf, exchange_name):
exchange._params = {'test': True} exchange._params = {'test': True}
assert exchange._get_params( assert exchange._get_params(
side="buy",
ordertype='limit', ordertype='limit',
reduceOnly=True, reduceOnly=True,
time_in_force='ioc', time_in_force='ioc',

View File

@ -1,7 +1,10 @@
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_get_maintenance_ratio_and_amt_okx( def test_get_maintenance_ratio_and_amt_okx(
@ -170,6 +173,70 @@ 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 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'}}])
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
# 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): def test_load_leverage_tiers_okx(default_conf, mocker, markets):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={ type(api_mock).has = PropertyMock(return_value={