Support both position modes on OKX

This commit is contained in:
Matthias 2022-05-07 10:56:13 +02:00
parent 2da284b921
commit 6fdcf3a10a
6 changed files with 129 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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',

View File

@ -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={