Merge pull request #8120 from freqtrade/fut/stop_price_type
stoploss price type
This commit is contained in:
commit
22cbc16238
@ -60,6 +60,7 @@
|
|||||||
"force_entry": "market",
|
"force_entry": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": false,
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_price_type": "last",
|
||||||
"stoploss_on_exchange_interval": 60,
|
"stoploss_on_exchange_interval": 60,
|
||||||
"stoploss_on_exchange_limit_ratio": 0.99
|
"stoploss_on_exchange_limit_ratio": 0.99
|
||||||
},
|
},
|
||||||
|
@ -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).
|
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.
|
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
|
||||||
|
|
||||||
`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.
|
`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.
|
||||||
|
@ -5,7 +5,7 @@ bot constants
|
|||||||
"""
|
"""
|
||||||
from typing import Any, Dict, List, Literal, Tuple
|
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'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
@ -25,6 +25,7 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other']
|
|||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
|
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
|
||||||
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
|
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
|
||||||
|
STOPLOSS_PRICE_TYPES = [p for p in PriceType]
|
||||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||||
@ -229,6 +230,7 @@ CONF_SCHEMA = {
|
|||||||
'default': 'market'},
|
'default': 'market'},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'},
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
|
'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES},
|
||||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||||
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
||||||
'maximum': 1.0}
|
'maximum': 1.0}
|
||||||
|
@ -6,6 +6,7 @@ from freqtrade.enums.exittype import ExitType
|
|||||||
from freqtrade.enums.hyperoptstate import HyperoptState
|
from freqtrade.enums.hyperoptstate import HyperoptState
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
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.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
||||||
|
8
freqtrade/enums/pricetype.py
Normal file
8
freqtrade/enums/pricetype.py
Normal file
@ -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"
|
@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
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.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
@ -33,6 +33,11 @@ class Binance(Exchange):
|
|||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
|
"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]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
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.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
@ -37,6 +37,12 @@ class Bybit(Exchange):
|
|||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"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]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
|
@ -24,6 +24,7 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN
|
|||||||
PairWithTimeframe)
|
PairWithTimeframe)
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
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 import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||||
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
@ -600,12 +601,27 @@ class Exchange:
|
|||||||
if not self.exchange_has('createMarketOrder'):
|
if not self.exchange_has('createMarketOrder'):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
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")
|
if (order_types.get("stoploss_on_exchange")
|
||||||
and not self._ft_has.get("stoploss_on_exchange", False)):
|
and not self._ft_has.get("stoploss_on_exchange", False)):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'On exchange stoploss is not supported for {self.name}.'
|
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:
|
def validate_pricing(self, pricing: Dict) -> None:
|
||||||
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
|
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
|
||||||
@ -1161,6 +1177,10 @@ class Exchange:
|
|||||||
stop_price=stop_price_norm)
|
stop_price=stop_price_norm)
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
params['reduceOnly'] = True
|
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))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
|
|
||||||
|
@ -34,6 +34,13 @@ class Gateio(Exchange):
|
|||||||
"needs_trading_fees": True,
|
"needs_trading_fees": True,
|
||||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
|
# 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]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
@ -49,6 +56,7 @@ class Gateio(Exchange):
|
|||||||
if any(v == 'market' for k, v in order_types.items()):
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
super().validate_stop_ordertypes(order_types)
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
|
@ -5,6 +5,7 @@ import ccxt
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
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, TemporaryError
|
||||||
from freqtrade.exchange import Exchange, date_minus_candles
|
from freqtrade.exchange import Exchange, date_minus_candles
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
@ -27,6 +28,12 @@ class Okx(Exchange):
|
|||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"tickers_have_quoteVolume": False,
|
"tickers_have_quoteVolume": False,
|
||||||
"fee_cost_in_contracts": True,
|
"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]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
|
@ -50,7 +50,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
|
|||||||
)
|
)
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order_types = {'stoploss': 'limit'}
|
order_types = {'stoploss': 'limit', 'stoploss_price_type': 'mark'}
|
||||||
if limitratio is not None:
|
if limitratio is not None:
|
||||||
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
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:
|
if trademode == TradingMode.SPOT:
|
||||||
params_dict = {'stopPrice': 220}
|
params_dict = {'stopPrice': 220}
|
||||||
else:
|
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
|
assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
|
@ -1060,6 +1060,47 @@ def test_validate_ordertypes(default_conf, mocker):
|
|||||||
Exchange(default_conf)
|
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),
|
||||||
|
('gate', 'last', False),
|
||||||
|
('gate', 'mark', False),
|
||||||
|
('gate', 'index', False),
|
||||||
|
])
|
||||||
|
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):
|
def test_validate_order_types_not_in_config(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
@ -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_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
mocker.patch('freqtrade.exchange.Exchange.name', 'Gate')
|
||||||
exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
|
exch = ExchangeResolver.load_exchange('gate', default_conf, True)
|
||||||
assert isinstance(exch, Gateio)
|
assert isinstance(exch, Gateio)
|
||||||
|
|
||||||
default_conf['order_types'] = {
|
default_conf['order_types'] = {
|
||||||
|
Loading…
Reference in New Issue
Block a user