From a2ce288241b8a77523b9608c1418fb306a8d95c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Jun 2022 21:05:15 +0200 Subject: [PATCH 01/11] Add okx stoploss on exchange (non-working for futures). --- freqtrade/exchange/exchange.py | 1 + freqtrade/exchange/okx.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e5f897c2a..728e997f1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1193,6 +1193,7 @@ class Exchange: try: params = self._get_stop_params(side=side, ordertype=ordertype, stop_price=stop_price_norm) + # TODO: reduceOnly is invalid for OKX stop orders if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has: diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index e7d658d24..048d4cad5 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -24,6 +24,8 @@ 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, @@ -157,3 +159,26 @@ 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 = super()._get_stop_params(side, ordertype, 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 fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + # TODO: This does not work until the algo-order is actually triggered! + return self.fetch_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + return self.cancel_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) From df20757d2116a52265edfbe4624ed545eddb9a23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Nov 2022 19:58:39 +0100 Subject: [PATCH 02/11] OKX stop: implement proper stoploss fetching --- freqtrade/exchange/okx.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 048d4cad5..8199bd0ea 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -6,7 +6,8 @@ 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 @@ -169,12 +170,23 @@ class Okx(Exchange): return params def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - # TODO: This does not work until the algo-order is actually triggered! - return self.fetch_order( - order_id=order_id, - pair=pair, - params={'stop': True} - ) + params1 = {'stop': True, 'ordType': 'trigger'} + for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, + self._api.fetch_canceled_orders): + try: + orders = method(pair, params=params1) + orders_f = [order for order in orders if order['id'] == order_id] + if orders_f: + order = orders_f[0] + if (order['status'] == 'closed' + and order.get('info', {}).get('ordId') is not None): + # Once a order triggered, we fetch the regular followup order. + return self.fetch_order(order['info']['ordId'], pair) + return order + except ccxt.BaseError: + logger.exception() + raise RetryableOrderError( + f'StoplossOrder not found (pair: {pair} id: {order_id}).') def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: return self.cancel_order( From 6c5dc7e0a9b8643dca66e0b1df79696a3eb7cd90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Nov 2022 20:24:26 +0100 Subject: [PATCH 03/11] OKX: improve stop order handling --- freqtrade/exchange/okx.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 8199bd0ea..4ff7f283b 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import ccxt @@ -10,6 +10,7 @@ from freqtrade.exceptions import (DDosProtection, OperationalException, Retryabl 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__) @@ -179,15 +180,26 @@ class Okx(Exchange): if orders_f: order = orders_f[0] if (order['status'] == 'closed' - and order.get('info', {}).get('ordId') is not None): + and (real_order_id := order.get('info', {}).get('ordId')) is not None): # Once a order triggered, we fetch the regular followup order. - return self.fetch_order(order['info']['ordId'], pair) + 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'] = 'stop' + order_reg['status_stop'] = 'triggered' + return order_reg return order except ccxt.BaseError: logger.exception() 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: return self.cancel_order( order_id=order_id, From d84ece7258a0082628f4459c278851fcac7374c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Nov 2022 20:17:10 +0100 Subject: [PATCH 04/11] Use conditional orders for stop orders --- freqtrade/exchange/okx.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 4ff7f283b..5acf039cb 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -164,14 +164,17 @@ class Okx(Exchange): def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - params = super()._get_stop_params(side, ordertype, stop_price) + 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 fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - params1 = {'stop': True, 'ordType': 'trigger'} + params1 = {'stop': True, 'ordType': 'conditional'} for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, self._api.fetch_canceled_orders): try: @@ -204,5 +207,5 @@ class Okx(Exchange): return self.cancel_order( order_id=order_id, pair=pair, - params={'stop': True} + params={'ordType': 'conditional'} ) From 224f289ec8fc6d91ee32e2a3d90f4749d4da9ca0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:19:56 +0100 Subject: [PATCH 05/11] OKX Stop: Add some more okx specific logic --- freqtrade/exchange/exchange.py | 1 - freqtrade/exchange/okx.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 728e997f1..e5f897c2a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1193,7 +1193,6 @@ class Exchange: try: params = self._get_stop_params(side=side, ordertype=ordertype, stop_price=stop_price_norm) - # TODO: reduceOnly is invalid for OKX stop orders if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has: diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 5acf039cb..5acfe7fcc 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -173,7 +173,30 @@ class Okx(Exchange): 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_order1', order_reg) + return order_reg + except ccxt.OrderNotFound: + pass params1 = {'stop': True, 'ordType': 'conditional'} for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, self._api.fetch_canceled_orders): @@ -192,9 +215,10 @@ class Okx(Exchange): order_reg['type'] = 'stop' order_reg['status_stop'] = 'triggered' return order_reg + order['type'] = 'stoploss' return order except ccxt.BaseError: - logger.exception() + pass raise RetryableOrderError( f'StoplossOrder not found (pair: {pair} id: {order_id}).') @@ -204,8 +228,11 @@ class Okx(Exchange): 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={'ordType': 'conditional'} + params=params1, ) From a7c7f720c0791d3ea2cdd5655626a8eab827813c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 20:03:34 +0100 Subject: [PATCH 06/11] Add test for okx fetch_stop --- freqtrade/exchange/okx.py | 2 +- tests/exchange/test_okx.py | 54 +++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 5acfe7fcc..7de110acf 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -212,7 +212,7 @@ class Okx(Exchange): self._log_exchange_response('fetch_stoploss_order1', order_reg) order_reg['id_stop'] = order_reg['id'] order_reg['id'] = order_id - order_reg['type'] = 'stop' + order_reg['type'] = 'stoploss' order_reg['status_stop'] = 'triggered' return order_reg order['type'] = 'stoploss' diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index fce77f4c7..30e23619b 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -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,53 @@ 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' From 2de5a59d890e2345eb1a935554b3453ab4047416 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:38:42 +0100 Subject: [PATCH 07/11] Add test for dry-run fetching --- tests/exchange/test_okx.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 30e23619b..2f862adda 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -528,3 +528,19 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): 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 From 4690244673175e708ed097ca22edcc4e9e432281 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:40:57 +0100 Subject: [PATCH 08/11] Enable okx stop-price types --- tests/exchange/test_exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7c48f1c9d..6e15abaf4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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), From 54d8aa7782160d5cf0da4074bf90e134205885fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:46:00 +0100 Subject: [PATCH 09/11] Test stoploss_adjust okx --- tests/exchange/test_okx.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 2f862adda..3b97e03f4 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -544,3 +544,18 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): 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) From 4f4bfdac4d2491277decca93cdd30aec9ec29a1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 09:00:00 +0100 Subject: [PATCH 10/11] Adjustments to okx stoploss --- freqtrade/exchange/okx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 7de110acf..3110d8189 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -32,7 +32,7 @@ class Okx(Exchange): _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", @@ -193,7 +193,7 @@ class Okx(Exchange): try: params1 = {'stop': True} order_reg = self._api.fetch_order(order_id, pair, params=params1) - self._log_exchange_response('fetch_stoploss_order1', order_reg) + self._log_exchange_response('fetch_stoploss_order', order_reg) return order_reg except ccxt.OrderNotFound: pass From 639987cbabc8ae969f280639ffa856af99402064 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 18:19:17 +0100 Subject: [PATCH 11/11] Prevent parameter reuse --- freqtrade/exchange/okx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 3110d8189..162630ea5 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -197,11 +197,11 @@ class Okx(Exchange): return order_reg except ccxt.OrderNotFound: pass - params1 = {'stop': True, 'ordType': 'conditional'} + 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=params1) + orders = method(pair, params=params2) orders_f = [order for order in orders if order['id'] == order_id] if orders_f: order = orders_f[0]