Merge branch 'feat/short' of https://github.com/freqtrade/freqtrade into feat/short
This commit is contained in:
commit
7e6b281b75
101
docs/hyperopt.md
101
docs/hyperopt.md
@ -253,7 +253,7 @@ We continue to define hyperoptable parameters:
|
|||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
|
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
|
||||||
buy_rsi = IntParameter(20, 40, default=30, space="buy")
|
buy_rsi = IntParameter(20, 40, default=30, space="buy")
|
||||||
buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy")
|
buy_adx_enabled = BooleanParameter(default=True, space="buy")
|
||||||
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
|
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
|
||||||
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
|
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
|
||||||
```
|
```
|
||||||
@ -316,6 +316,7 @@ There are four parameter types each suited for different purposes.
|
|||||||
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
|
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
|
||||||
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
|
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
|
||||||
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
||||||
|
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
||||||
|
|
||||||
!!! Tip "Disabling parameter optimization"
|
!!! Tip "Disabling parameter optimization"
|
||||||
Each parameter takes two boolean parameters:
|
Each parameter takes two boolean parameters:
|
||||||
@ -326,7 +327,7 @@ There are four parameter types each suited for different purposes.
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
||||||
|
|
||||||
### Optimizing an indicator parameter
|
## Optimizing an indicator parameter
|
||||||
|
|
||||||
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
||||||
|
|
||||||
@ -336,8 +337,8 @@ from functools import reduce
|
|||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
IStrategy, IntParameter)
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
@ -413,6 +414,98 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
|||||||
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
|
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
|
||||||
You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space.
|
You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space.
|
||||||
|
|
||||||
|
## Optimizing protections
|
||||||
|
|
||||||
|
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
|
||||||
|
|
||||||
|
The strategy will simply need to define the "protections" entry as property returning a list of protection configurations.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from pandas import DataFrame
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
|
||||||
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
|
IStrategy, IntParameter)
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
stoploss = -0.05
|
||||||
|
timeframe = '15m'
|
||||||
|
# Define the parameter spaces
|
||||||
|
cooldown_lookback = IntParameter(2, 48, default=5, space="protection", optimize=True)
|
||||||
|
stop_duration = IntParameter(12, 200, default=5, space="protection", optimize=True)
|
||||||
|
use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
prot = []
|
||||||
|
|
||||||
|
prot.append({
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": self.cooldown_lookback.value
|
||||||
|
})
|
||||||
|
if self.use_stop_protection.value:
|
||||||
|
prot.append({
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period_candles": 24 * 3,
|
||||||
|
"trade_limit": 4,
|
||||||
|
"stop_duration_candles": self.stop_duration.value,
|
||||||
|
"only_per_pair": False
|
||||||
|
})
|
||||||
|
|
||||||
|
return protection
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# ...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run hyperopt as follows:
|
||||||
|
`freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy --spaces protection`
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The protection space is not part of the default space, and is only available with the Parameters Hyperopt interface, not with the legacy hyperopt interface (which required separate hyperopt files).
|
||||||
|
Freqtrade will also automatically change the "--enable-protections" flag if the protection space is selected.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
If protections are defined as property, entries from the configuration will be ignored.
|
||||||
|
It is therefore recommended to not define protections in the configuration.
|
||||||
|
|
||||||
|
### Migrating from previous property setups
|
||||||
|
|
||||||
|
A migration from a previous setup is pretty simple, and can be accomplished by converting the protections entry to a property.
|
||||||
|
In simple terms, the following configuration will be converted to the below.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
protections = [
|
||||||
|
{
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Result
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
|
||||||
|
|
||||||
## Loss-functions
|
## Loss-functions
|
||||||
|
|
||||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||||
|
@ -15,6 +15,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||||||
!!! Note "Backtesting"
|
!!! Note "Backtesting"
|
||||||
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
||||||
|
|
||||||
|
!!! Warning "Setting protections from the configuration"
|
||||||
|
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
|
||||||
|
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
|
||||||
|
|
||||||
### Available Protections
|
### Available Protections
|
||||||
|
|
||||||
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
||||||
@ -47,7 +51,9 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
|
|||||||
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "StoplossGuard",
|
"method": "StoplossGuard",
|
||||||
"lookback_period_candles": 24,
|
"lookback_period_candles": 24,
|
||||||
@ -55,7 +61,7 @@ protections = [
|
|||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
"only_per_pair": False
|
"only_per_pair": False
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
@ -69,7 +75,9 @@ protections = [
|
|||||||
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
@ -77,7 +85,7 @@ protections = [
|
|||||||
"stop_duration_candles": 12,
|
"stop_duration_candles": 12,
|
||||||
"max_allowed_drawdown": 0.2
|
"max_allowed_drawdown": 0.2
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Low Profit Pairs
|
#### Low Profit Pairs
|
||||||
@ -88,7 +96,9 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur
|
|||||||
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
|
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "LowProfitPairs",
|
"method": "LowProfitPairs",
|
||||||
"lookback_period_candles": 6,
|
"lookback_period_candles": 6,
|
||||||
@ -96,7 +106,7 @@ protections = [
|
|||||||
"stop_duration": 60,
|
"stop_duration": 60,
|
||||||
"required_profit": 0.02
|
"required_profit": 0.02
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cooldown Period
|
#### Cooldown Period
|
||||||
@ -106,12 +116,14 @@ protections = [
|
|||||||
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration_candles": 2
|
"stop_duration_candles": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
@ -136,7 +148,10 @@ from freqtrade.strategy import IStrategy
|
|||||||
|
|
||||||
class AwesomeStrategy(IStrategy)
|
class AwesomeStrategy(IStrategy)
|
||||||
timeframe = '1h'
|
timeframe = '1h'
|
||||||
protections = [
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration_candles": 5
|
"stop_duration_candles": 5
|
||||||
|
@ -218,7 +218,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"spaces": Arg(
|
"spaces": Arg(
|
||||||
'--spaces',
|
'--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default='default',
|
default='default',
|
||||||
),
|
),
|
||||||
|
@ -110,3 +110,6 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||||
)
|
)
|
||||||
config['timeframe'] = config['ticker_interval']
|
config['timeframe'] = config['ticker_interval']
|
||||||
|
|
||||||
|
if 'protections' in config:
|
||||||
|
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||||
|
@ -146,6 +146,8 @@ class Backtesting:
|
|||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-sell is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
|
||||||
|
def _load_protections(self, strategy: IStrategy):
|
||||||
if self.config.get('enable_protections', False):
|
if self.config.get('enable_protections', False):
|
||||||
conf = self.config
|
conf = self.config
|
||||||
if hasattr(strategy, 'protections'):
|
if hasattr(strategy, 'protections'):
|
||||||
@ -194,6 +196,7 @@ class Backtesting:
|
|||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
self.rejected_trades = 0
|
self.rejected_trades = 0
|
||||||
self.dataprovider.clear_cache()
|
self.dataprovider.clear_cache()
|
||||||
|
self._load_protections(self.strategy)
|
||||||
|
|
||||||
def check_abort(self):
|
def check_abort(self):
|
||||||
"""
|
"""
|
||||||
|
@ -66,6 +66,7 @@ class Hyperopt:
|
|||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.buy_space: List[Dimension] = []
|
self.buy_space: List[Dimension] = []
|
||||||
self.sell_space: List[Dimension] = []
|
self.sell_space: List[Dimension] = []
|
||||||
|
self.protection_space: List[Dimension] = []
|
||||||
self.roi_space: List[Dimension] = []
|
self.roi_space: List[Dimension] = []
|
||||||
self.stoploss_space: List[Dimension] = []
|
self.stoploss_space: List[Dimension] = []
|
||||||
self.trailing_space: List[Dimension] = []
|
self.trailing_space: List[Dimension] = []
|
||||||
@ -191,6 +192,8 @@ class Hyperopt:
|
|||||||
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
||||||
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space}
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
result['roi'] = {str(k): v for k, v in
|
result['roi'] = {str(k): v for k, v in
|
||||||
self.custom_hyperopt.generate_roi_table(params).items()}
|
self.custom_hyperopt.generate_roi_table(params).items()}
|
||||||
@ -241,6 +244,12 @@ class Hyperopt:
|
|||||||
"""
|
"""
|
||||||
Assign the dimensions in the hyperoptimization space.
|
Assign the dimensions in the hyperoptimization space.
|
||||||
"""
|
"""
|
||||||
|
if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
# Protections can only be optimized when using the Parameter interface
|
||||||
|
logger.debug("Hyperopt has 'protection' space")
|
||||||
|
# Enable Protections if protection space is selected.
|
||||||
|
self.config['enable_protections'] = True
|
||||||
|
self.protection_space = self.custom_hyperopt.protection_space()
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'buy'):
|
if HyperoptTools.has_space(self.config, 'buy'):
|
||||||
logger.debug("Hyperopt has 'buy' space")
|
logger.debug("Hyperopt has 'buy' space")
|
||||||
@ -261,8 +270,8 @@ class Hyperopt:
|
|||||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||||
logger.debug("Hyperopt has 'trailing' space")
|
logger.debug("Hyperopt has 'trailing' space")
|
||||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||||
self.dimensions = (self.buy_space + self.sell_space + self.roi_space +
|
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||||
self.stoploss_space + self.trailing_space)
|
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||||
|
|
||||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||||
"""
|
"""
|
||||||
@ -282,6 +291,12 @@ class Hyperopt:
|
|||||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'):
|
||||||
|
if attr.optimize:
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
attr.value = params_dict[attr_name]
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||||
|
@ -73,6 +73,9 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
def sell_indicator_space(self) -> List['Dimension']:
|
def sell_indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
return self._get_indicator_space('sell', 'sell_indicator_space')
|
||||||
|
|
||||||
|
def protection_space(self) -> List['Dimension']:
|
||||||
|
return self._get_indicator_space('protection', 'indicator_space')
|
||||||
|
|
||||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||||
return self._get_func('generate_roi_table')(params)
|
return self._get_func('generate_roi_table')(params)
|
||||||
|
|
||||||
|
@ -57,6 +57,13 @@ class IHyperOpt(ABC):
|
|||||||
"""
|
"""
|
||||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
||||||
|
|
||||||
|
def protection_space(self) -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create a protection space.
|
||||||
|
Only supported by the Parameter interface.
|
||||||
|
"""
|
||||||
|
raise OperationalException(_format_exception_message('indicator_space', 'protection'))
|
||||||
|
|
||||||
def indicator_space(self) -> List[Dimension]:
|
def indicator_space(self) -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Create an indicator space.
|
Create an indicator space.
|
||||||
|
@ -82,8 +82,8 @@ class HyperoptTools():
|
|||||||
"""
|
"""
|
||||||
Tell if the space value is contained in the configuration
|
Tell if the space value is contained in the configuration
|
||||||
"""
|
"""
|
||||||
# The 'trailing' space is not included in the 'default' set of spaces
|
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||||
if space == 'trailing':
|
if space in ('trailing', 'protection'):
|
||||||
return any(s in config['spaces'] for s in [space, 'all'])
|
return any(s in config['spaces'] for s in [space, 'all'])
|
||||||
else:
|
else:
|
||||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||||
@ -149,7 +149,7 @@ class HyperoptTools():
|
|||||||
|
|
||||||
if print_json:
|
if print_json:
|
||||||
result_dict: Dict = {}
|
result_dict: Dict = {}
|
||||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
||||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||||
|
|
||||||
@ -158,6 +158,8 @@ class HyperoptTools():
|
|||||||
non_optimized)
|
non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||||
non_optimized)
|
non_optimized)
|
||||||
|
HyperoptTools._params_pretty_print(params, 'protection',
|
||||||
|
"Protection hyperspace params:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||||
|
@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC):
|
|||||||
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._protection_config = protection_config
|
self._protection_config = protection_config
|
||||||
|
self._stop_duration_candles: Optional[int] = None
|
||||||
|
self._lookback_period_candles: Optional[int] = None
|
||||||
|
|
||||||
tf_in_min = timeframe_to_minutes(config['timeframe'])
|
tf_in_min = timeframe_to_minutes(config['timeframe'])
|
||||||
if 'stop_duration_candles' in protection_config:
|
if 'stop_duration_candles' in protection_config:
|
||||||
self._stop_duration_candles = protection_config.get('stop_duration_candles', 1)
|
self._stop_duration_candles = int(protection_config.get('stop_duration_candles', 1))
|
||||||
self._stop_duration = (tf_in_min * self._stop_duration_candles)
|
self._stop_duration = (tf_in_min * self._stop_duration_candles)
|
||||||
else:
|
else:
|
||||||
self._stop_duration_candles = None
|
self._stop_duration_candles = None
|
||||||
self._stop_duration = protection_config.get('stop_duration', 60)
|
self._stop_duration = protection_config.get('stop_duration', 60)
|
||||||
if 'lookback_period_candles' in protection_config:
|
if 'lookback_period_candles' in protection_config:
|
||||||
self._lookback_period_candles = protection_config.get('lookback_period_candles', 1)
|
self._lookback_period_candles = int(protection_config.get('lookback_period_candles', 1))
|
||||||
self._lookback_period = tf_in_min * self._lookback_period_candles
|
self._lookback_period = tf_in_min * self._lookback_period_candles
|
||||||
else:
|
else:
|
||||||
self._lookback_period_candles = None
|
self._lookback_period_candles = None
|
||||||
self._lookback_period = protection_config.get('lookback_period', 60)
|
self._lookback_period = int(protection_config.get('lookback_period', 60))
|
||||||
|
|
||||||
LoggingMixin.__init__(self, logger)
|
LoggingMixin.__init__(self, logger)
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class StrategyResolver(IResolver):
|
|||||||
- default (if not None)
|
- default (if not None)
|
||||||
"""
|
"""
|
||||||
if (attribute in config
|
if (attribute in config
|
||||||
and not isinstance(getattr(type(strategy), 'my_property', None), property)):
|
and not isinstance(getattr(type(strategy), attribute, None), property)):
|
||||||
# Ensure Properties are not overwritten
|
# Ensure Properties are not overwritten
|
||||||
setattr(strategy, attribute, config[attribute])
|
setattr(strategy, attribute, config[attribute])
|
||||||
logger.info("Override strategy '%s' with value in config file: %s.",
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
|
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_prev_date, timeframe_to_seconds)
|
||||||
from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter,
|
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
RealParameter)
|
IntParameter, RealParameter)
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
||||||
|
@ -270,6 +270,28 @@ class CategoricalParameter(BaseParameter):
|
|||||||
return [self.value]
|
return [self.value]
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanParameter(CategoricalParameter):
|
||||||
|
|
||||||
|
def __init__(self, *, default: Optional[Any] = None,
|
||||||
|
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize hyperopt-optimizable Boolean Parameter.
|
||||||
|
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||||
|
:param default: A default value. If not specified, first item from specified space will be
|
||||||
|
used.
|
||||||
|
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||||
|
parameter field
|
||||||
|
name is prefixed with 'buy_' or 'sell_'.
|
||||||
|
:param optimize: Include parameter in hyperopt optimizations.
|
||||||
|
:param load: Load parameter value from {space}_params.
|
||||||
|
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||||
|
"""
|
||||||
|
|
||||||
|
categories = [True, False]
|
||||||
|
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||||
|
load=load, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class HyperStrategyMixin(object):
|
class HyperStrategyMixin(object):
|
||||||
"""
|
"""
|
||||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||||
@ -283,6 +305,7 @@ class HyperStrategyMixin(object):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.ft_buy_params: List[BaseParameter] = []
|
self.ft_buy_params: List[BaseParameter] = []
|
||||||
self.ft_sell_params: List[BaseParameter] = []
|
self.ft_sell_params: List[BaseParameter] = []
|
||||||
|
self.ft_protection_params: List[BaseParameter] = []
|
||||||
|
|
||||||
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
||||||
|
|
||||||
@ -292,11 +315,12 @@ class HyperStrategyMixin(object):
|
|||||||
:param category:
|
:param category:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if category not in ('buy', 'sell', None):
|
if category not in ('buy', 'sell', 'protection', None):
|
||||||
raise OperationalException('Category must be one of: "buy", "sell", None.')
|
raise OperationalException(
|
||||||
|
'Category must be one of: "buy", "sell", "protection", None.')
|
||||||
|
|
||||||
if category is None:
|
if category is None:
|
||||||
params = self.ft_buy_params + self.ft_sell_params
|
params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params
|
||||||
else:
|
else:
|
||||||
params = getattr(self, f"ft_{category}_params")
|
params = getattr(self, f"ft_{category}_params")
|
||||||
|
|
||||||
@ -324,9 +348,10 @@ class HyperStrategyMixin(object):
|
|||||||
params: Dict = {
|
params: Dict = {
|
||||||
'buy': list(cls.detect_parameters('buy')),
|
'buy': list(cls.detect_parameters('buy')),
|
||||||
'sell': list(cls.detect_parameters('sell')),
|
'sell': list(cls.detect_parameters('sell')),
|
||||||
|
'protection': list(cls.detect_parameters('protection')),
|
||||||
}
|
}
|
||||||
params.update({
|
params.update({
|
||||||
'count': len(params['buy'] + params['sell'])
|
'count': len(params['buy'] + params['sell'] + params['protection'])
|
||||||
})
|
})
|
||||||
|
|
||||||
return params
|
return params
|
||||||
@ -340,9 +365,12 @@ class HyperStrategyMixin(object):
|
|||||||
self._ft_params_from_file = params
|
self._ft_params_from_file = params
|
||||||
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
|
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
|
||||||
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
|
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
|
||||||
|
protection_params = deep_merge_dicts(params.get('protection', {}),
|
||||||
|
getattr(self, 'protection_params', {}))
|
||||||
|
|
||||||
self._load_params(buy_params, 'buy', hyperopt)
|
self._load_params(buy_params, 'buy', hyperopt)
|
||||||
self._load_params(sell_params, 'sell', hyperopt)
|
self._load_params(sell_params, 'sell', hyperopt)
|
||||||
|
self._load_params(protection_params, 'protection', hyperopt)
|
||||||
|
|
||||||
def load_params_from_file(self) -> Dict:
|
def load_params_from_file(self) -> Dict:
|
||||||
filename_str = getattr(self, '__file__', '')
|
filename_str = getattr(self, '__file__', '')
|
||||||
@ -397,7 +425,8 @@ class HyperStrategyMixin(object):
|
|||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
'buy': {},
|
'buy': {},
|
||||||
'sell': {}
|
'sell': {},
|
||||||
|
'protection': {},
|
||||||
}
|
}
|
||||||
for name, p in self.enumerate_parameters():
|
for name, p in self.enumerate_parameters():
|
||||||
if not p.optimize or not p.in_space:
|
if not p.optimize or not p.in_space:
|
||||||
|
@ -6,8 +6,8 @@ import numpy as np # noqa
|
|||||||
import pandas as pd # noqa
|
import pandas as pd # noqa
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
IStrategy, IntParameter)
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
# Add your lib to import here
|
# Add your lib to import here
|
||||||
|
@ -6,8 +6,8 @@ import numpy as np # noqa
|
|||||||
import pandas as pd # noqa
|
import pandas as pd # noqa
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
IStrategy, IntParameter)
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
# Add your lib to import here
|
# Add your lib to import here
|
||||||
|
@ -577,6 +577,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
"20.0": 0.02,
|
"20.0": 0.02,
|
||||||
"50.0": 0.01,
|
"50.0": 0.01,
|
||||||
"110.0": 0},
|
"110.0": 0},
|
||||||
|
'protection': {},
|
||||||
'sell': {'sell-adx-enabled': False,
|
'sell': {'sell-adx-enabled': False,
|
||||||
'sell-adx-value': 0,
|
'sell-adx-value': 0,
|
||||||
'sell-fastd-enabled': True,
|
'sell-fastd-enabled': True,
|
||||||
@ -592,7 +593,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
'trailing_stop_positive': 0.02,
|
'trailing_stop_positive': 0.02,
|
||||||
'trailing_stop_positive_offset': 0.07}},
|
'trailing_stop_positive_offset': 0.07}},
|
||||||
'params_dict': optimizer_param,
|
'params_dict': optimizer_param,
|
||||||
'params_not_optimized': {'buy': {}, 'sell': {}},
|
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
|
||||||
'results_metrics': ANY,
|
'results_metrics': ANY,
|
||||||
'total_profit': 3.1e-08
|
'total_profit': 3.1e-08
|
||||||
}
|
}
|
||||||
@ -1002,6 +1003,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
hyperopt_conf.update({
|
hyperopt_conf.update({
|
||||||
'strategy': 'HyperoptableStrategy',
|
'strategy': 'HyperoptableStrategy',
|
||||||
'user_data_dir': Path(tmpdir),
|
'user_data_dir': Path(tmpdir),
|
||||||
|
'hyperopt_random_state': 42,
|
||||||
|
'spaces': ['all']
|
||||||
})
|
})
|
||||||
hyperopt = Hyperopt(hyperopt_conf)
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
@ -1009,12 +1012,18 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
|
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
|
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
|
||||||
|
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
|
||||||
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
||||||
assert isinstance(buy_rsi_range, range)
|
assert isinstance(buy_rsi_range, range)
|
||||||
# Range from 0 - 50 (inclusive)
|
# Range from 0 - 50 (inclusive)
|
||||||
assert len(list(buy_rsi_range)) == 51
|
assert len(list(buy_rsi_range)) == 51
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
# All values should've changed.
|
||||||
|
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
|
||||||
|
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
|
||||||
|
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
|
||||||
|
|
||||||
|
|
||||||
def test_SKDecimal():
|
def test_SKDecimal():
|
||||||
|
@ -4,7 +4,8 @@ import talib.abstract as ta
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter
|
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
||||||
|
RealParameter)
|
||||||
|
|
||||||
|
|
||||||
class HyperoptableStrategy(IStrategy):
|
class HyperoptableStrategy(IStrategy):
|
||||||
@ -64,6 +65,18 @@ class HyperoptableStrategy(IStrategy):
|
|||||||
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
||||||
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
||||||
load=False)
|
load=False)
|
||||||
|
protection_enabled = BooleanParameter(default=True)
|
||||||
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
prot = []
|
||||||
|
if self.protection_enabled.value:
|
||||||
|
prot.append({
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": self.protection_cooldown_lookback.value
|
||||||
|
})
|
||||||
|
return prot
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
|
@ -16,8 +16,8 @@ from freqtrade.exceptions import OperationalException, StrategyError
|
|||||||
from freqtrade.optimize.space import SKDecimal
|
from freqtrade.optimize.space import SKDecimal
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter,
|
from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||||
IntParameter, RealParameter)
|
DecimalParameter, IntParameter, RealParameter)
|
||||||
from freqtrade.strategy.interface import SellCheckTuple
|
from freqtrade.strategy.interface import SellCheckTuple
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from tests.conftest import log_has, log_has_re
|
from tests.conftest import log_has, log_has_re
|
||||||
@ -717,6 +717,17 @@ def test_hyperopt_parameters():
|
|||||||
assert len(list(catpar.range)) == 3
|
assert len(list(catpar.range)) == 3
|
||||||
assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none']
|
assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none']
|
||||||
|
|
||||||
|
boolpar = BooleanParameter(default=True, space='buy')
|
||||||
|
assert boolpar.value is True
|
||||||
|
assert isinstance(boolpar.get_space(''), Categorical)
|
||||||
|
assert isinstance(boolpar.range, list)
|
||||||
|
assert len(list(boolpar.range)) == 1
|
||||||
|
|
||||||
|
boolpar.in_space = True
|
||||||
|
assert len(list(boolpar.range)) == 2
|
||||||
|
|
||||||
|
assert list(boolpar.range) == [True, False]
|
||||||
|
|
||||||
|
|
||||||
def test_auto_hyperopt_interface(default_conf):
|
def test_auto_hyperopt_interface(default_conf):
|
||||||
default_conf.update({'strategy': 'HyperoptableStrategy'})
|
default_conf.update({'strategy': 'HyperoptableStrategy'})
|
||||||
@ -734,7 +745,8 @@ def test_auto_hyperopt_interface(default_conf):
|
|||||||
assert isinstance(all_params, dict)
|
assert isinstance(all_params, dict)
|
||||||
assert len(all_params['buy']) == 2
|
assert len(all_params['buy']) == 2
|
||||||
assert len(all_params['sell']) == 2
|
assert len(all_params['sell']) == 2
|
||||||
assert all_params['count'] == 4
|
# Number of Hyperoptable parameters
|
||||||
|
assert all_params['count'] == 6
|
||||||
|
|
||||||
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')
|
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')
|
||||||
|
|
||||||
|
@ -1330,7 +1330,7 @@ def test_process_removed_setting(mocker, default_conf, caplog):
|
|||||||
'sectionB', 'somesetting')
|
'sectionB', 'somesetting')
|
||||||
|
|
||||||
|
|
||||||
def test_process_deprecated_ticker_interval(mocker, default_conf, caplog):
|
def test_process_deprecated_ticker_interval(default_conf, caplog):
|
||||||
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
|
||||||
config = deepcopy(default_conf)
|
config = deepcopy(default_conf)
|
||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
@ -1352,6 +1352,17 @@ def test_process_deprecated_ticker_interval(mocker, default_conf, caplog):
|
|||||||
process_temporary_deprecated_settings(config)
|
process_temporary_deprecated_settings(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_deprecated_protections(default_conf, caplog):
|
||||||
|
message = "DEPRECATED: Setting 'protections' in the configuration is deprecated."
|
||||||
|
config = deepcopy(default_conf)
|
||||||
|
process_temporary_deprecated_settings(config)
|
||||||
|
assert not log_has(message, caplog)
|
||||||
|
|
||||||
|
config['protections'] = []
|
||||||
|
process_temporary_deprecated_settings(config)
|
||||||
|
assert log_has(message, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_flat_vars_to_nested_dict(caplog):
|
def test_flat_vars_to_nested_dict(caplog):
|
||||||
|
|
||||||
test_args = {
|
test_args = {
|
||||||
|
Loading…
Reference in New Issue
Block a user