From a9241f61f9f1241c8aa9ebbe3d817d26e6590b5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Feb 2023 13:33:09 +0100 Subject: [PATCH 01/11] Add Price Type Enum --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/pricetype.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 freqtrade/enums/pricetype.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index eb70a2894..8ef53e12d 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -6,6 +6,7 @@ from freqtrade.enums.exittype import ExitType from freqtrade.enums.hyperoptstate import HyperoptState from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.ordertypevalue import OrderTypeValues +from freqtrade.enums.pricetype import PriceType from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType diff --git a/freqtrade/enums/pricetype.py b/freqtrade/enums/pricetype.py new file mode 100644 index 000000000..bf0922b9f --- /dev/null +++ b/freqtrade/enums/pricetype.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class PriceType(str, Enum): + """Enum to distinguish possible trigger prices for stoplosses""" + LAST = "last" + MARK = "mark" + INDEX = "index" From c4fc811619819a12e9b5f59cf7677d328b7bce6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Feb 2023 17:38:39 +0100 Subject: [PATCH 02/11] Add stop_price_type support (futures only!). --- freqtrade/exchange/binance.py | 7 ++++++- freqtrade/exchange/bybit.py | 8 +++++++- freqtrade/exchange/exchange.py | 5 +++++ freqtrade/exchange/okx.py | 7 +++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 22dfdc1d1..94800f59c 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple import arrow import ccxt -from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -32,6 +32,11 @@ class Binance(Exchange): _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "tickers_have_price": False, + "stop_price_type_field": "workingType", + "stop_price_type_value_mapping": { + PriceType.LAST: "CONTRACT_PRICE", + PriceType.MARK: "MARK_PRICE", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index d0598d8de..c565b891f 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, TradingMode +from freqtrade.enums import MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -37,6 +37,12 @@ class Bybit(Exchange): "funding_fee_timeframe": "8h", "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "limit", "market": "market"}, + "stop_price_type_field": "triggerBy", + "stop_price_type_value_mapping": { + PriceType.LAST: "LastPrice", + PriceType.MARK: "MarkPrice", + PriceType.INDEX: "IndexPrice", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8ac2abf6c..253df8607 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,6 +23,7 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN PairWithTimeframe) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode +from freqtrade.enums.pricetype import PriceType from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -1160,6 +1161,10 @@ class Exchange: stop_price=stop_price_norm) 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: + price_type = self._ft_has['stop_price_type_value_mapping'][ + order_types.get('stoploss_price_type', PriceType.LAST)] + params[self._ft_has['stop_price_type_field']] = price_type amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 6792c2cba..4c9f38c57 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -5,6 +5,7 @@ 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.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier @@ -27,6 +28,12 @@ class Okx(Exchange): _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, "fee_cost_in_contracts": True, + "stop_price_type_field": "tpTriggerPxType", + "stop_price_type_value_mapping": { + PriceType.LAST: "last", + PriceType.MARK: "index", + PriceType.INDEX: "mark", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ From 2738c3784559f30d7a19a85ff205cab883f11225 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Feb 2023 20:37:13 +0100 Subject: [PATCH 03/11] Test stoploss validation ... --- freqtrade/exchange/exchange.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 253df8607..fda775547 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -606,6 +606,16 @@ class Exchange: raise OperationalException( f'On exchange stoploss is not supported for {self.name}.' ) + if self.trading_mode == TradingMode.FUTURES: + price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys() + if ( + order_types.get("stoploss_on_exchange", False) is True + and 'stoploss_price_type' in order_types + and order_types['stoploss_price_type'] not in price_mapping + ): + raise OperationalException( + f'On exchange stoploss price type is not supported for {self.name}.' + ) def validate_pricing(self, pricing: Dict) -> None: if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): From cf9e99b8e1a4732aad30a13500613f3860af589d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Feb 2023 20:44:17 +0100 Subject: [PATCH 04/11] Add tests for ordertype validation --- tests/exchange/test_exchange.py | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0ebdfd218..6b28b33b6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1060,6 +1060,44 @@ def test_validate_ordertypes(default_conf, mocker): Exchange(default_conf) +@pytest.mark.parametrize('exchange_name,stopadv, expected', [ + ('binance', 'last', True), + ('binance', 'mark', True), + ('binance', 'index', False), + ('bybit', 'last', True), + ('bybit', 'mark', True), + ('bybit', 'index', True), + # ('okx', 'last', True), + # ('okx', 'mark', True), + # ('okx', 'index', True), + ]) +def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, stopadv, expected): + + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + default_conf['order_types'] = { + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True, + 'stoploss_price_type': stopadv, + } + if expected: + ExchangeResolver.load_exchange(exchange_name, default_conf) + else: + with pytest.raises(OperationalException, + match=r'On exchange stoploss price type is not supported for .*'): + ExchangeResolver.load_exchange(exchange_name, default_conf) + + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) From 3497de3dd5f7bb0a554a475c21b35e8f9171a56f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Feb 2023 21:08:41 +0100 Subject: [PATCH 05/11] Add more validation --- freqtrade/constants.py | 4 +++- tests/exchange/test_binance.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b41a3ad9c..08048c3e7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,7 +5,7 @@ bot constants """ from typing import Any, Dict, List, Literal, Tuple -from freqtrade.enums import CandleType, RPCMessageType +from freqtrade.enums import CandleType, PriceType, RPCMessageType DEFAULT_CONFIG = 'config.json' @@ -25,6 +25,7 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] _ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO'] ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES] +STOPLOSS_PRICE_TYPES = [p for p in PriceType] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', @@ -229,6 +230,7 @@ CONF_SCHEMA = { 'default': 'market'}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, + 'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES}, 'stoploss_on_exchange_interval': {'type': 'number'}, 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, 'maximum': 1.0} diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index cb304f699..4d0602609 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -50,7 +50,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side ) api_mock.create_order.reset_mock() - order_types = {'stoploss': 'limit'} + order_types = {'stoploss': 'limit', 'stoploss_price_type': 'mark'} if limitratio is not None: order_types.update({'stoploss_on_exchange_limit_ratio': limitratio}) @@ -75,7 +75,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side if trademode == TradingMode.SPOT: params_dict = {'stopPrice': 220} else: - params_dict = {'stopPrice': 220, 'reduceOnly': True} + params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'} assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict # test exception handling From b8a527e4a08ad0f5e0c7bfd52e0ec49113644883 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Feb 2023 10:46:24 +0100 Subject: [PATCH 06/11] Add gateio price type field --- freqtrade/exchange/gateio.py | 8 +++++++- tests/exchange/test_exchange.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index de178af02..cc7e47cca 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, TradingMode +from freqtrade.enums import MarginMode, PriceType, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.misc import safe_value_fallback2 @@ -34,6 +34,12 @@ class Gateio(Exchange): "needs_trading_fees": True, "fee_cost_in_contracts": False, # Set explicitly to false for clarity "order_props_in_contracts": ['amount', 'filled', 'remaining'], + "stop_price_type_field": "price_type", + "stop_price_type_value_mapping": { + PriceType.LAST: 0, + PriceType.MARK: 1, + PriceType.INDEX: 2, + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6b28b33b6..dfa807b95 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1070,6 +1070,9 @@ def test_validate_ordertypes(default_conf, mocker): # ('okx', 'last', True), # ('okx', 'mark', True), # ('okx', 'index', True), + ('gate', 'last', True), + ('gate', 'mark', True), + ('gate', 'index', True), ]) def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, stopadv, expected): From d904e916631a6cd568c17381e39ec518e2b90e75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Feb 2023 14:53:52 +0100 Subject: [PATCH 07/11] Add documentation for new setting --- docs/stoploss.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/stoploss.md b/docs/stoploss.md index 20e53d8f5..1182b6d90 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -52,6 +52,17 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. +### stoploss_price_type + +!!! Warning "Only applies to futures" + `stoploss_price_type` only applies to futures markets (on exchanges where it's available). + Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected. + +Stoploss on exchange on futures markets can trigger on different price types. +The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index". + +Acceptable values for this setting are `"last"`, `"mark"` and `"index"` - which freqtrade will transfer automatically to the corresponding API type, and place the [stoploss on exchange](#stoploss_on_exchange-and-stoploss_on_exchange_limit_ratio) order correspondingly. + ### force_exit `force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API. From e964377edf7b3ebcc4a9c7ea40e7d53e8269e186 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Feb 2023 14:58:12 +0100 Subject: [PATCH 08/11] Add new field to full config --- config_examples/config_full.example.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index b60957b58..64e5b76ea 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -60,6 +60,7 @@ "force_entry": "market", "stoploss": "market", "stoploss_on_exchange": false, + "stoploss_price_type": "last", "stoploss_on_exchange_interval": 60, "stoploss_on_exchange_limit_ratio": 0.99 }, From 8c0c2496c279cc13a792cd03ff4691f6f78fc988 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Feb 2023 07:13:01 +0100 Subject: [PATCH 09/11] Temporarily disable gate advanced stop orders --- freqtrade/exchange/gateio.py | 15 +++++++++------ tests/exchange/test_exchange.py | 6 +++--- tests/exchange/test_gateio.py | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index cc7e47cca..8960e163d 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -34,12 +34,13 @@ class Gateio(Exchange): "needs_trading_fees": True, "fee_cost_in_contracts": False, # Set explicitly to false for clarity "order_props_in_contracts": ['amount', 'filled', 'remaining'], - "stop_price_type_field": "price_type", - "stop_price_type_value_mapping": { - PriceType.LAST: 0, - PriceType.MARK: 1, - PriceType.INDEX: 2, - }, + # TODO: Reenable once https://github.com/ccxt/ccxt/issues/16749 is available + # "stop_price_type_field": "price_type", + # "stop_price_type_value_mapping": { + # PriceType.LAST: 0, + # PriceType.MARK: 1, + # PriceType.INDEX: 2, + # }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -55,6 +56,8 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') + else: + super().validate_ordertypes(order_types) def _get_params( self, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index dfa807b95..90341142a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1070,9 +1070,9 @@ def test_validate_ordertypes(default_conf, mocker): # ('okx', 'last', True), # ('okx', 'mark', True), # ('okx', 'index', True), - ('gate', 'last', True), - ('gate', 'mark', True), - ('gate', 'index', True), + ('gate', 'last', False), + ('gate', 'mark', False), + ('gate', 'index', False), ]) def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, stopadv, expected): diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index dabdbba65..9802063e8 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -18,8 +18,8 @@ def test_validate_order_types_gateio(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') - exch = ExchangeResolver.load_exchange('gateio', default_conf, True) + mocker.patch('freqtrade.exchange.Exchange.name', 'Gate') + exch = ExchangeResolver.load_exchange('gate', default_conf, True) assert isinstance(exch, Gateio) default_conf['order_types'] = { From 953be8a7f86fbb832d377930dd78cd0ab5d6cc04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Feb 2023 18:00:44 +0100 Subject: [PATCH 10/11] Split validate_order_types to 2 functions to allow selective application --- freqtrade/exchange/exchange.py | 5 +++++ freqtrade/exchange/gateio.py | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fda775547..aa34d6156 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -600,7 +600,12 @@ class Exchange: if not self.exchange_has('createMarketOrder'): raise OperationalException( f'Exchange {self.name} does not support market orders.') + self.validate_stop_ordertypes(order_types) + def validate_stop_ordertypes(self, order_types: Dict) -> None: + """ + Validate stoploss order types + """ if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): raise OperationalException( diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 8960e163d..48302c522 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -56,8 +56,7 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') - else: - super().validate_ordertypes(order_types) + super().validate_stop_ordertypes(order_types) def _get_params( self, From 5a61e076d79aa9bdc98f5eb2477ce3a0494cde87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Feb 2023 19:19:59 +0100 Subject: [PATCH 11/11] Remove unused import --- freqtrade/exchange/gateio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 48302c522..247e4e954 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, PriceType, TradingMode +from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.misc import safe_value_fallback2