Merge pull request #8360 from freqtrade/okx_stop

Okx stoploss on exchange
This commit is contained in:
Matthias 2023-03-20 19:19:59 +01:00 committed by GitHub
commit 7b5e322ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 169 additions and 7 deletions

View File

@ -1,14 +1,16 @@
import logging
from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
TemporaryError)
from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@ -24,11 +26,13 @@ class Okx(Exchange):
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
"stop_price_type_field": "tpTriggerPxType",
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
@ -157,3 +161,78 @@ class Okx(Exchange):
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['maxNotional'] / leverage
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopLossPrice': stop_price})
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, True)
return params
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
OKX uses non-default stoploss price naming.
"""
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
return (
order.get('stopLossPrice', None) is None
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
)
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
params1 = {'stop': True}
order_reg = self._api.fetch_order(order_id, pair, params=params1)
self._log_exchange_response('fetch_stoploss_order', order_reg)
return order_reg
except ccxt.OrderNotFound:
pass
params2 = {'stop': True, 'ordType': 'conditional'}
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
self._api.fetch_canceled_orders):
try:
orders = method(pair, params=params2)
orders_f = [order for order in orders if order['id'] == order_id]
if orders_f:
order = orders_f[0]
if (order['status'] == '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)
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['type'] = 'stoploss'
return order
except ccxt.BaseError:
pass
raise RetryableOrderError(
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
params1 = {'stop': True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)

View File

@ -1039,9 +1039,9 @@ def test_validate_ordertypes(default_conf, mocker):
('bybit', 'last', True),
('bybit', 'mark', True),
('bybit', 'index', True),
# ('okx', 'last', True),
# ('okx', 'mark', True),
# ('okx', 'index', True),
('okx', 'last', True),
('okx', 'mark', True),
('okx', 'index', True),
('gate', 'last', True),
('gate', 'mark', True),
('gate', 'index', True),

View File

@ -2,11 +2,13 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import RetryableOrderError
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_mock_coro, get_patched_exchange, log_has
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@ -476,3 +478,84 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog,
exchange.load_leverage_tiers()
assert log_has(logmsg, caplog)
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_okx(default_conf, mocker):
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_order = MagicMock()
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx')
exchange.fetch_stoploss_order('1234', 'ETH/BTC')
assert api_mock.fetch_order.call_count == 1
assert api_mock.fetch_order.call_args_list[0][0][0] == '1234'
assert api_mock.fetch_order.call_args_list[0][0][1] == 'ETH/BTC'
assert api_mock.fetch_order.call_args_list[0][1]['params'] == {'stop': True}
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound)
api_mock.fetch_open_orders = MagicMock(return_value=[])
api_mock.fetch_closed_orders = MagicMock(return_value=[])
api_mock.fetch_canceled_orders = MagicMock(creturn_value=[])
with pytest.raises(RetryableOrderError):
exchange.fetch_stoploss_order('1234', 'ETH/BTC')
assert api_mock.fetch_order.call_count == 1
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 1
assert api_mock.fetch_canceled_orders.call_count == 1
api_mock.fetch_order.reset_mock()
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock()
api_mock.fetch_canceled_orders.reset_mock()
api_mock.fetch_closed_orders = MagicMock(return_value=[
{
'id': '1234',
'status': 'closed',
'info': {'ordId': '123455'}
}
])
mocker.patch(f"{EXMS}.fetch_order", MagicMock(return_value={'id': '123455'}))
resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC')
assert api_mock.fetch_order.call_count == 1
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 1
assert api_mock.fetch_canceled_orders.call_count == 0
assert resp['id'] == '1234'
assert resp['id_stop'] == '123455'
assert resp['type'] == 'stoploss'
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx')
dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={'id': '123455'}))
api_mock.fetch_order.reset_mock()
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock()
api_mock.fetch_canceled_orders.reset_mock()
resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC')
assert api_mock.fetch_order.call_count == 0
assert api_mock.fetch_open_orders.call_count == 0
assert api_mock.fetch_closed_orders.call_count == 0
assert api_mock.fetch_canceled_orders.call_count == 0
assert dro_mock.call_count == 1
@pytest.mark.parametrize('sl1,sl2,sl3,side', [
(1501, 1499, 1501, "sell"),
(1499, 1501, 1499, "buy")
])
def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
exchange = get_patched_exchange(mocker, default_conf, id='okx')
order = {
'type': 'stoploss',
'price': 1500,
'stopLossPrice': 1500,
}
assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(sl2, order, side=side)