Merge branch 'develop' into feat/short

This commit is contained in:
Matthias 2021-08-07 09:42:25 +02:00
commit 92ed7c0bf8
46 changed files with 422 additions and 204 deletions

View File

@ -26,8 +26,8 @@ hesitate to read the source code and understand the mechanism of this bot.
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_

View File

@ -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.

View File

@ -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,15 +51,17 @@ 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):
"method": "StoplossGuard", return [
"lookback_period_candles": 24, {
"trade_limit": 4, "method": "StoplossGuard",
"stop_duration_candles": 4, "lookback_period_candles": 24,
"only_per_pair": False "trade_limit": 4,
} "stop_duration_candles": 4,
] "only_per_pair": False
}
]
``` ```
!!! Note !!! Note
@ -69,15 +75,17 @@ 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):
"method": "MaxDrawdown", return [
"lookback_period_candles": 48, {
"trade_limit": 20, "method": "MaxDrawdown",
"stop_duration_candles": 12, "lookback_period_candles": 48,
"max_allowed_drawdown": 0.2 "trade_limit": 20,
}, "stop_duration_candles": 12,
] "max_allowed_drawdown": 0.2
},
]
``` ```
#### Low Profit Pairs #### Low Profit Pairs
@ -88,15 +96,17 @@ 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):
"method": "LowProfitPairs", return [
"lookback_period_candles": 6, {
"trade_limit": 2, "method": "LowProfitPairs",
"stop_duration": 60, "lookback_period_candles": 6,
"required_profit": 0.02 "trade_limit": 2,
} "stop_duration": 60,
] "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):
"method": "CooldownPeriod", return [
"stop_duration_candles": 2 {
} "method": "CooldownPeriod",
] "stop_duration_candles": 2
}
]
``` ```
!!! Note !!! Note
@ -136,39 +148,42 @@ from freqtrade.strategy import IStrategy
class AwesomeStrategy(IStrategy) class AwesomeStrategy(IStrategy)
timeframe = '1h' timeframe = '1h'
protections = [
{ @property
"method": "CooldownPeriod", def protections(self):
"stop_duration_candles": 5 return [
}, {
{ "method": "CooldownPeriod",
"method": "MaxDrawdown", "stop_duration_candles": 5
"lookback_period_candles": 48, },
"trade_limit": 20, {
"stop_duration_candles": 4, "method": "MaxDrawdown",
"max_allowed_drawdown": 0.2 "lookback_period_candles": 48,
}, "trade_limit": 20,
{ "stop_duration_candles": 4,
"method": "StoplossGuard", "max_allowed_drawdown": 0.2
"lookback_period_candles": 24, },
"trade_limit": 4, {
"stop_duration_candles": 2, "method": "StoplossGuard",
"only_per_pair": False "lookback_period_candles": 24,
}, "trade_limit": 4,
{ "stop_duration_candles": 2,
"method": "LowProfitPairs", "only_per_pair": False
"lookback_period_candles": 6, },
"trade_limit": 2, {
"stop_duration_candles": 60, "method": "LowProfitPairs",
"required_profit": 0.02 "lookback_period_candles": 6,
}, "trade_limit": 2,
{ "stop_duration_candles": 60,
"method": "LowProfitPairs", "required_profit": 0.02
"lookback_period_candles": 24, },
"trade_limit": 4, {
"stop_duration_candles": 2, "method": "LowProfitPairs",
"required_profit": 0.01 "lookback_period_candles": 24,
} "trade_limit": 4,
] "stop_duration_candles": 2,
"required_profit": 0.01
}
]
# ... # ...
``` ```

View File

@ -36,7 +36,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists)) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)

View File

@ -193,7 +193,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile=f"subtemplates/exchange_{exchange_template}.j2", templatefile=f"subtemplates/exchange_{exchange_template}.j2",
arguments=selections arguments=selections
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile="subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2",

View File

@ -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',
), ),

View File

@ -38,15 +38,15 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
indicators = render_template_with_fallback( indicators = render_template_with_fallback(
templatefile=f"subtemplates/indicators_{subtemplate}.j2", templatefile=f"subtemplates/indicators_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/indicators_{fallback}.j2", templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
) )
buy_trend = render_template_with_fallback( buy_trend = render_template_with_fallback(
templatefile=f"subtemplates/buy_trend_{subtemplate}.j2", templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2", templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
) )
sell_trend = render_template_with_fallback( sell_trend = render_template_with_fallback(
templatefile=f"subtemplates/sell_trend_{subtemplate}.j2", templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2", templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
) )
plot_config = render_template_with_fallback( plot_config = render_template_with_fallback(
templatefile=f"subtemplates/plot_config_{subtemplate}.j2", templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2", templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
@ -97,19 +97,19 @@ def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: st
buy_guards = render_template_with_fallback( buy_guards = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
) )
sell_guards = render_template_with_fallback( sell_guards = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
) )
buy_space = render_template_with_fallback( buy_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
) )
sell_space = render_template_with_fallback( sell_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
) )
strategy_text = render_template(templatefile='base_hyperopt.py.j2', strategy_text = render_template(templatefile='base_hyperopt.py.j2',
arguments={"hyperopt": hyperopt_name, arguments={"hyperopt": hyperopt_name,

View File

@ -187,7 +187,7 @@ def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> Li
x for x in epochs x for x in epochs
if x['results_metrics'].get( if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades') 'trade_count', x['results_metrics'].get('total_trades')
) < filteroptions['filter_max_trades'] ) < filteroptions['filter_max_trades']
] ]
return epochs return epochs
@ -239,7 +239,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
x for x in epochs x for x in epochs
if x['results_metrics'].get( if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) < filteroptions['filter_max_avg_profit'] ) < filteroptions['filter_max_avg_profit']
] ]
if filteroptions['filter_min_total_profit'] is not None: if filteroptions['filter_min_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
@ -247,7 +247,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
x for x in epochs x for x in epochs
if x['results_metrics'].get( if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0) 'profit', x['results_metrics'].get('profit_total_abs', 0)
) > filteroptions['filter_min_total_profit'] ) > filteroptions['filter_min_total_profit']
] ]
if filteroptions['filter_max_total_profit'] is not None: if filteroptions['filter_max_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
@ -255,7 +255,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
x for x in epochs x for x in epochs
if x['results_metrics'].get( if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0) 'profit', x['results_metrics'].get('profit_total_abs', 0)
) < filteroptions['filter_max_total_profit'] ) < filteroptions['filter_max_total_profit']
] ]
return epochs return epochs

View File

@ -51,10 +51,10 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
if not is_exchange_known_ccxt(exchange): if not is_exchange_known_ccxt(exchange):
raise OperationalException( raise OperationalException(
f'Exchange "{exchange}" is not known to the ccxt library ' f'Exchange "{exchange}" is not known to the ccxt library '
f'and therefore not available for the bot.\n' f'and therefore not available for the bot.\n'
f'The following exchanges are available for Freqtrade: ' f'The following exchanges are available for Freqtrade: '
f'{", ".join(available_exchanges())}' f'{", ".join(available_exchanges())}'
) )
valid, reason = validate_exchange(exchange) valid, reason = validate_exchange(exchange)

View File

@ -115,7 +115,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0: if conf.get('stoploss') == 0.0:
raise OperationalException( raise OperationalException(
'The config stoploss needs to be different from 0 to avoid problems with sell orders.' 'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
) )
# Skip if trailing stoploss is not activated # Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False): if not conf.get('trailing_stop', False):
return return
@ -180,7 +180,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
raise OperationalException( raise OperationalException(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n" "Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
) )
if ('lookback_period' in prot and 'lookback_period_candles' in prot): if ('lookback_period' in prot and 'lookback_period_candles' in prot):
raise OperationalException( raise OperationalException(

View File

@ -108,5 +108,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
raise OperationalException( raise OperationalException(
"Both 'timeframe' and 'ticker_interval' detected." "Both 'timeframe' and 'ticker_interval' detected."
"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.")

View File

@ -280,7 +280,7 @@ CONF_SCHEMA = {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
}, },
} }
}, },
'reload': {'type': 'boolean'}, 'reload': {'type': 'boolean'},

View File

@ -231,12 +231,12 @@ class Edge:
'Minimum expectancy and minimum winrate are met only for %s,' 'Minimum expectancy and minimum winrate are met only for %s,'
' so other pairs are filtered out.', ' so other pairs are filtered out.',
self._final_pairs self._final_pairs
) )
else: else:
logger.info( logger.info(
'Edge removed all pairs as no pair with minimum expectancy ' 'Edge removed all pairs as no pair with minimum expectancy '
'and minimum winrate was found !' 'and minimum winrate was found !'
) )
return self._final_pairs return self._final_pairs
@ -247,7 +247,7 @@ class Edge:
final = [] final = []
for pair, info in self._cached_pairs.items(): for pair, info in self._cached_pairs.items():
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
final.append({ final.append({
'Pair': pair, 'Pair': pair,
'Winrate': info.winrate, 'Winrate': info.winrate,

View File

@ -44,7 +44,7 @@ def main(sysargv: List[str] = None) -> None:
"as `freqtrade trade [options...]`.\n" "as `freqtrade trade [options...]`.\n"
"To see the full list of options available, please use " "To see the full list of options available, please use "
"`freqtrade --help` or `freqtrade <command> --help`." "`freqtrade --help` or `freqtrade <command> --help`."
) )
except SystemExit as e: except SystemExit as e:
return_code = e return_code = e

View File

@ -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):
""" """

View File

@ -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))
@ -444,9 +459,9 @@ class Hyperopt:
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
] ]
with progressbar.ProgressBar( with progressbar.ProgressBar(
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
widgets=widgets widgets=widgets
) as pbar: ) as pbar:
EVALS = ceil(self.total_epochs / jobs) EVALS = ceil(self.total_epochs / jobs)
for i in range(EVALS): for i in range(EVALS):
# Correct the number of epochs to be processed for the last # Correct the number of epochs to be processed for the last

View File

@ -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)

View File

@ -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.

View File

@ -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)
@ -203,7 +205,7 @@ class HyperoptTools():
elif space == "roi": elif space == "roi":
result = result[:-1] + f'{appendix}\n' result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({ minimal_roi_result = rapidjson.dumps({
str(k): v for k, v in (space_params or no_params).items() str(k): v for k, v in (space_params or no_params).items()
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE) }, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
result += f"minimal_roi = {minimal_roi_result}" result += f"minimal_roi = {minimal_roi_result}"
elif space == "trailing": elif space == "trailing":

View File

@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
filename = Path.joinpath( filename = Path.joinpath(
recordfilename.parent, recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
).with_suffix(recordfilename.suffix) ).with_suffix(recordfilename.suffix)
file_dump_json(filename, stats) file_dump_json(filename, stats)
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
@ -173,7 +173,7 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
for strategy, results in all_results.items(): for strategy, results in all_results.items():
tabular_data.append(_generate_result_line( tabular_data.append(_generate_result_line(
results['results'], results['config']['dry_run_wallet'], strategy) results['results'], results['config']['dry_run_wallet'], strategy)
) )
try: try:
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
value_col='profit_ratio') value_col='profit_ratio')
@ -604,7 +604,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency']) strat_results['stake_currency'])
stake_amount = round_coin_value( stake_amount = round_coin_value(
strat_results['stake_amount'], strat_results['stake_currency'] strat_results['stake_amount'], strat_results['stake_currency']
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited' ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
message = ("No trades made. " message = ("No trades made. "
f"Your starting balance was {start_balance}, " f"Your starting balance was {start_balance}, "

View File

@ -334,8 +334,8 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
) )
elif indicator_b not in data: elif indicator_b not in data:
logger.info( logger.info(
'fill_to: "%s" ignored. Reason: This indicator is not ' 'fill_to: "%s" ignored. Reason: This indicator is not '
'in your strategy.', indicator_b 'in your strategy.', indicator_b
) )
return fig return fig

View File

@ -144,7 +144,7 @@ class IPairList(LoggingMixin, ABC):
markets = self._exchange.markets markets = self._exchange.markets
if not markets: if not markets:
raise OperationalException( raise OperationalException(
'Markets not loaded. Make sure that exchange is initialized correctly.') 'Markets not loaded. Make sure that exchange is initialized correctly.')
sanitized_whitelist: List[str] = [] sanitized_whitelist: List[str] = []
for pair in pairlist: for pair in pairlist:

View File

@ -120,9 +120,9 @@ class VolumePairList(IPairList):
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
filtered_tickers = [ filtered_tickers = [
v for k, v in tickers.items() v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and v[self._sort_key] is not None)] and v[self._sort_key] is not None)]
pairlist = [s['symbol'] for s in filtered_tickers] pairlist = [s['symbol'] for s in filtered_tickers]
pairlist = self.filter_pairlist(pairlist, tickers) pairlist = self.filter_pairlist(pairlist, tickers)
@ -197,7 +197,7 @@ class VolumePairList(IPairList):
if self._min_value > 0: if self._min_value > 0:
filtered_tickers = [ filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] > self._min_value] v for v in filtered_tickers if v[self._sort_key] > self._min_value]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key]) sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])

View File

@ -28,13 +28,13 @@ class PairListManager():
self._tickers_needed = False self._tickers_needed = False
for pairlist_handler_config in self._config.get('pairlists', None): for pairlist_handler_config in self._config.get('pairlists', None):
pairlist_handler = PairListResolver.load_pairlist( pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'], pairlist_handler_config['method'],
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pairlist_handler_config, pairlistconfig=pairlist_handler_config,
pairlist_pos=len(self._pairlist_handlers) pairlist_pos=len(self._pairlist_handlers)
) )
self._tickers_needed |= pairlist_handler.needstickers self._tickers_needed |= pairlist_handler.needstickers
self._pairlist_handlers.append(pairlist_handler) self._pairlist_handlers.append(pairlist_handler)

View File

@ -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)

View File

@ -54,9 +54,9 @@ class StoplossGuard(IProtection):
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value) SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < 0)] and trade.close_profit and trade.close_profit < 0)]
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:
return False, None, None return False, None, None

View File

@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from freqtrade.resolvers.pairlist_resolver import PairListResolver from freqtrade.resolvers.pairlist_resolver import PairListResolver
from freqtrade.resolvers.protection_resolver import ProtectionResolver from freqtrade.resolvers.protection_resolver import ProtectionResolver
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver

View File

@ -50,7 +50,7 @@ class StrategyResolver(IResolver):
if 'timeframe' not in config: if 'timeframe' not in config:
logger.warning( logger.warning(
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'." "DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
) )
strategy.timeframe = strategy.ticker_interval strategy.timeframe = strategy.ticker_interval
if strategy._ft_params_from_file: if strategy._ft_params_from_file:
@ -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.",

View File

@ -199,8 +199,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
config=Depends(get_config)): config=Depends(get_config)):
config = deepcopy(config) config = deepcopy(config)
config.update({ config.update({
'strategy': strategy, 'strategy': strategy,
}) })
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)

View File

@ -62,7 +62,7 @@ class CryptoToFiatConverter:
# If the request is not a 429 error we want to raise the normal error # If the request is not a 429 error we want to raise the normal error
logger.error( logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: {}".format( "Could not load FIAT Cryptocurrency map for the following problem: {}".format(
request_exception request_exception
) )
) )
except (Exception) as exception: except (Exception) as exception:

View File

@ -15,6 +15,7 @@ class RPCManager:
""" """
Class to manage RPC objects (Telegram, API, ...) Class to manage RPC objects (Telegram, API, ...)
""" """
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" Initializes all enabled rpc modules """ """ Initializes all enabled rpc modules """
self.registered_modules: List[RPCHandler] = [] self.registered_modules: List[RPCHandler] = []

View File

@ -77,7 +77,6 @@ class Telegram(RPCHandler):
""" This class handles all telegram communication """ """ This class handles all telegram communication """
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
""" """
Init the Telegram call, and init the super class RPCHandler Init the Telegram call, and init the super class RPCHandler
:param rpc: instance of RPC Helper class :param rpc: instance of RPC Helper class
@ -270,7 +269,7 @@ class Telegram(RPCHandler):
noti = '' noti = ''
if msg_type == RPCMessageType.SELL: if msg_type == RPCMessageType.SELL:
sell_noti = self._config['telegram'] \ sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {}) .get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string # For backward compatibility sell still can be string
if isinstance(sell_noti, str): if isinstance(sell_noti, str):
noti = sell_noti noti = sell_noti
@ -278,7 +277,7 @@ class Telegram(RPCHandler):
noti = sell_noti.get(str(msg['sell_reason']), default_noti) noti = sell_noti.get(str(msg['sell_reason']), default_noti)
else: else:
noti = self._config['telegram'] \ noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), default_noti) .get('notification_settings', {}).get(str(msg_type), default_noti)
if noti == 'off': if noti == 'off':
logger.info(f"Notification '{msg_type}' not sent.") logger.info(f"Notification '{msg_type}' not sent.")
@ -541,7 +540,7 @@ class Telegram(RPCHandler):
f"`{first_trade_date}`\n" f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Latest Trade opened:* `{latest_trade_date}\n`"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
) )
if stats['closed_trade_count'] > 0: if stats['closed_trade_count'] > 0:
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
@ -576,13 +575,14 @@ class Telegram(RPCHandler):
sell_reasons_msg = tabulate( sell_reasons_msg = tabulate(
sell_reasons_tabulate, sell_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
) )
durations = stats['durations'] durations = stats['durations']
duration_msg = tabulate([ duration_msg = tabulate(
['Wins', str(timedelta(seconds=durations['wins'])) [
if durations['wins'] != 'N/A' else 'N/A'], ['Wins', str(timedelta(seconds=durations['wins']))
['Losses', str(timedelta(seconds=durations['losses'])) if durations['wins'] != 'N/A' else 'N/A'],
if durations['losses'] != 'N/A' else 'N/A'] ['Losses', str(timedelta(seconds=durations['losses']))
if durations['losses'] != 'N/A' else 'N/A']
], ],
headers=['', 'Avg. Duration'] headers=['', 'Avg. Duration']
) )
@ -1100,7 +1100,7 @@ class Telegram(RPCHandler):
if reload_able: if reload_able:
reply_markup = InlineKeyboardMarkup([ reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)], [InlineKeyboardButton("Refresh", callback_data=callback_path)],
]) ])
else: else:
reply_markup = InlineKeyboardMarkup([[]]) reply_markup = InlineKeyboardMarkup([[]])
msg += "\nUpdated: {}".format(datetime.now().ctime()) msg += "\nUpdated: {}".format(datetime.now().ctime())

View File

@ -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

View File

@ -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:

View File

@ -38,7 +38,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
informative['date_merge'] = ( informative['date_merge'] = (
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
) )
else: else:
raise ValueError("Tried to merge a faster timeframe to a slower timeframe." raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
"This would create new rows, and can throw off your regular indicators.") "This would create new rows, and can throw off your regular indicators.")

View File

@ -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

View File

@ -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

View File

@ -6,8 +6,8 @@
*/ */
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD", // C++-style comment "fiat_display_currency": "USD", // C++-style comment
"amount_reserve_percent" : 0.05, // And more, tabs before this comment "amount_reserve_percent": 0.05, // And more, tabs before this comment
"dry_run": false, "dry_run": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,
@ -15,15 +15,15 @@
"trailing_stop_positive_offset": 0.0051, "trailing_stop_positive_offset": 0.0051,
"trailing_only_offset_is_reached": false, "trailing_only_offset_is_reached": false,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
"20": 0.02, "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
"sell": 30, // Trailing comma should also be accepted now "sell": 30, // Trailing comma should also be accepted now
}, },
"bid_strategy": { "bid_strategy": {
"use_order_book": false, "use_order_book": false,
@ -34,7 +34,7 @@
"bids_to_ask_delta": 1 "bids_to_ask_delta": 1
} }
}, },
"ask_strategy":{ "ask_strategy": {
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9
@ -64,7 +64,9 @@
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "", "password": "",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": false, "enableRateLimit": false,
"rateLimit": 500, "rateLimit": 500,
@ -103,8 +105,8 @@
"remove_pumps": false "remove_pumps": false
}, },
"telegram": { "telegram": {
// We can now comment out some settings // We can now comment out some settings
// "enabled": true, // "enabled": true,
"enabled": false, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id"
@ -124,4 +126,4 @@
}, },
"strategy": "DefaultStrategy", "strategy": "DefaultStrategy",
"strategy_path": "user_data/strategies/" "strategy_path": "user_data/strategies/"
} }

View File

@ -399,7 +399,7 @@ def test_hyperopt_format_results(hyperopt):
'rejected_signals': 2, 'rejected_signals': 2,
'backtest_start_time': 1619718665, 'backtest_start_time': 1619718665,
'backtest_end_time': 1619718665, 'backtest_end_time': 1619718665,
} }
results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result,
Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 11, 14, 19, 32, 00),
Arrow(2017, 12, 14, 19, 32, 00), market_change=0) Arrow(2017, 12, 14, 19, 32, 00), market_change=0)
@ -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():

View File

@ -93,7 +93,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, min_ago_open=200, min_ago_close=30,
)) ))
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
@ -150,7 +150,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, profit_rate=0.9, min_ago_open=200, min_ago_close=30, profit_rate=0.9,
)) ))
assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.stop_per_pair(pair)
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()

View File

@ -139,9 +139,9 @@ def test_fiat_too_many_requests_response(mocker, caplog):
assert length_cryptomap == 0 assert length_cryptomap == 0
assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert fiat_convert._backoff > datetime.datetime.now().timestamp()
assert log_has( assert log_has(
'Too many requests for Coingecko API, backing off and trying again later.', 'Too many requests for Coingecko API, backing off and trying again later.',
caplog caplog
) )
def test_fiat_invalid_response(mocker, caplog): def test_fiat_invalid_response(mocker, caplog):

View File

@ -942,7 +942,7 @@ def test_api_whitelist(botclient):
"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
"length": 4, "length": 4,
"method": ["StaticPairList"] "method": ["StaticPairList"]
} }
def test_api_forcebuy(botclient, mocker, fee): def test_api_forcebuy(botclient, mocker, fee):
@ -1033,7 +1033,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
} }
def test_api_forcesell(botclient, mocker, ticker, fee, markets): def test_api_forcesell(botclient, mocker, ticker, fee, markets):
@ -1215,7 +1215,7 @@ def test_api_strategies(botclient):
'DefaultStrategy', 'DefaultStrategy',
'HyperoptableStrategy', 'HyperoptableStrategy',
'TestStrategyLegacy' 'TestStrategyLegacy'
]} ]}
def test_api_strategy(botclient): def test_api_strategy(botclient):

View File

@ -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):
""" """

View File

@ -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')

View File

@ -125,7 +125,7 @@ def test_parse_args_backtesting_custom() -> None:
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'DefaultStrategy',
'SampleStrategy' 'SampleStrategy'
] ]
call_args = Arguments(args).get_parsed_arg() call_args = Arguments(args).get_parsed_arg()
assert call_args['config'] == ['test_conf.json'] assert call_args['config'] == ['test_conf.json']
assert call_args['verbosity'] == 0 assert call_args['verbosity'] == 0

View File

@ -1130,17 +1130,17 @@ def test_pairlist_resolving_fallback(mocker):
@pytest.mark.parametrize("setting", [ @pytest.mark.parametrize("setting", [
("ask_strategy", "use_sell_signal", True, ("ask_strategy", "use_sell_signal", True,
None, "use_sell_signal", False), None, "use_sell_signal", False),
("ask_strategy", "sell_profit_only", True, ("ask_strategy", "sell_profit_only", True,
None, "sell_profit_only", False), None, "sell_profit_only", False),
("ask_strategy", "sell_profit_offset", 0.1, ("ask_strategy", "sell_profit_offset", 0.1,
None, "sell_profit_offset", 0.01), None, "sell_profit_offset", 0.01),
("ask_strategy", "ignore_roi_if_buy_signal", True, ("ask_strategy", "ignore_roi_if_buy_signal", True,
None, "ignore_roi_if_buy_signal", False), None, "ignore_roi_if_buy_signal", False),
("ask_strategy", "ignore_buying_expired_candle_after", 5, ("ask_strategy", "ignore_buying_expired_candle_after", 5,
None, "ignore_buying_expired_candle_after", 6), None, "ignore_buying_expired_candle_after", 6),
]) ])
def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -1180,10 +1180,10 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
@pytest.mark.parametrize("setting", [ @pytest.mark.parametrize("setting", [
("experimental", "use_sell_signal", False), ("experimental", "use_sell_signal", False),
("experimental", "sell_profit_only", True), ("experimental", "sell_profit_only", True),
("experimental", "ignore_roi_if_buy_signal", True), ("experimental", "ignore_roi_if_buy_signal", True),
]) ])
def test_process_removed_settings(mocker, default_conf, setting): def test_process_removed_settings(mocker, default_conf, setting):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -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 = {