Support both position modes on OKX
This commit is contained in:
parent
2da284b921
commit
6fdcf3a10a
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
@ -944,6 +953,7 @@ class Exchange:
|
|||||||
|
|
||||||
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
|
||||||
|
@ -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)
|
||||||
@ -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,
|
||||||
|
@ -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:
|
||||||
|
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
|
||||||
|
@ -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',
|
||||||
|
@ -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,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
|
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):
|
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={
|
||||||
|
Loading…
Reference in New Issue
Block a user