diff --git a/README.md b/README.md index 8cba78136..78ea3cecd 100644 --- a/README.md +++ b/README.md @@ -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. +- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) - [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] [FTX](https://ftx.com) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 4fba925d0..96f9ff177 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -253,7 +253,7 @@ We continue to define hyperoptable parameters: class MyAwesomeStrategy(IStrategy): buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, 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_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. * `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. +* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters. !!! Tip "Disabling parameter optimization" Each parameter takes two boolean parameters: @@ -326,7 +327,7 @@ There are four parameter types each suited for different purposes. !!! 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. -### 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. @@ -336,8 +337,8 @@ from functools import reduce import talib.abstract as ta -from freqtrade.strategy import IStrategy -from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) import freqtrade.vendor.qtpylib.indicators as qtpylib 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). 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 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. diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 5dcc83738..0757d2f6d 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -15,6 +15,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Note "Backtesting" 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 * [`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. ``` python -protections = [ - { - "method": "StoplossGuard", - "lookback_period_candles": 24, - "trade_limit": 4, - "stop_duration_candles": 4, - "only_per_pair": False - } -] +@property +def protections(self): + return [ + { + "method": "StoplossGuard", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 4, + "only_per_pair": False + } + ] ``` !!! 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. ``` python -protections = [ - { - "method": "MaxDrawdown", - "lookback_period_candles": 48, - "trade_limit": 20, - "stop_duration_candles": 12, - "max_allowed_drawdown": 0.2 - }, -] +@property +def protections(self): + return [ + { + "method": "MaxDrawdown", + "lookback_period_candles": 48, + "trade_limit": 20, + "stop_duration_candles": 12, + "max_allowed_drawdown": 0.2 + }, + ] ``` #### 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. ``` python -protections = [ - { - "method": "LowProfitPairs", - "lookback_period_candles": 6, - "trade_limit": 2, - "stop_duration": 60, - "required_profit": 0.02 - } -] +@property +def protections(self): + return [ + { + "method": "LowProfitPairs", + "lookback_period_candles": 6, + "trade_limit": 2, + "stop_duration": 60, + "required_profit": 0.02 + } + ] ``` #### 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". ``` python -protections = [ - { - "method": "CooldownPeriod", - "stop_duration_candles": 2 - } -] +@property +def protections(self): + return [ + { + "method": "CooldownPeriod", + "stop_duration_candles": 2 + } + ] ``` !!! Note @@ -136,39 +148,42 @@ from freqtrade.strategy import IStrategy class AwesomeStrategy(IStrategy) timeframe = '1h' - protections = [ - { - "method": "CooldownPeriod", - "stop_duration_candles": 5 - }, - { - "method": "MaxDrawdown", - "lookback_period_candles": 48, - "trade_limit": 20, - "stop_duration_candles": 4, - "max_allowed_drawdown": 0.2 - }, - { - "method": "StoplossGuard", - "lookback_period_candles": 24, - "trade_limit": 4, - "stop_duration_candles": 2, - "only_per_pair": False - }, - { - "method": "LowProfitPairs", - "lookback_period_candles": 6, - "trade_limit": 2, - "stop_duration_candles": 60, - "required_profit": 0.02 - }, - { - "method": "LowProfitPairs", - "lookback_period_candles": 24, - "trade_limit": 4, - "stop_duration_candles": 2, - "required_profit": 0.01 - } - ] + + @property + def protections(self): + return [ + { + "method": "CooldownPeriod", + "stop_duration_candles": 5 + }, + { + "method": "MaxDrawdown", + "lookback_period_candles": 48, + "trade_limit": 20, + "stop_duration_candles": 4, + "max_allowed_drawdown": 0.2 + }, + { + "method": "StoplossGuard", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 2, + "only_per_pair": False + }, + { + "method": "LowProfitPairs", + "lookback_period_candles": 6, + "trade_limit": 2, + "stop_duration_candles": 60, + "required_profit": 0.02 + }, + { + "method": "LowProfitPairs", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 2, + "required_profit": 0.01 + } + ] # ... ``` diff --git a/docs/index.md b/docs/index.md index 8077cd303..05eaa7552 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. -- [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] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index b3f912433..852cab92e 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -193,7 +193,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None: selections['exchange'] = render_template( templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections - ) + ) except TemplateNotFound: selections['exchange'] = render_template( templatefile="subtemplates/exchange_generic.j2", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f56a2bf18..215ed3f6e 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -218,7 +218,7 @@ AVAILABLE_CLI_OPTIONS = { "spaces": Arg( '--spaces', 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='+', default='default', ), diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index cc0d653b9..eb65579e2 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -38,15 +38,15 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st indicators = render_template_with_fallback( templatefile=f"subtemplates/indicators_{subtemplate}.j2", templatefallbackfile=f"subtemplates/indicators_{fallback}.j2", - ) + ) buy_trend = render_template_with_fallback( templatefile=f"subtemplates/buy_trend_{subtemplate}.j2", templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2", - ) + ) sell_trend = render_template_with_fallback( templatefile=f"subtemplates/sell_trend_{subtemplate}.j2", templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2", - ) + ) plot_config = render_template_with_fallback( templatefile=f"subtemplates/plot_config_{subtemplate}.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( templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2", - ) + ) sell_guards = render_template_with_fallback( templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2", - ) + ) buy_space = render_template_with_fallback( templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2", - ) + ) sell_space = render_template_with_fallback( templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2", - ) + ) strategy_text = render_template(templatefile='base_hyperopt.py.j2', arguments={"hyperopt": hyperopt_name, diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 5a2727795..4694d1111 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -187,7 +187,7 @@ def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> Li x for x in epochs if x['results_metrics'].get( 'trade_count', x['results_metrics'].get('total_trades') - ) < filteroptions['filter_max_trades'] + ) < filteroptions['filter_max_trades'] ] return epochs @@ -239,7 +239,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: x for x in epochs if x['results_metrics'].get( '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: 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 if x['results_metrics'].get( '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: 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 if x['results_metrics'].get( 'profit', x['results_metrics'].get('profit_total_abs', 0) - ) < filteroptions['filter_max_total_profit'] + ) < filteroptions['filter_max_total_profit'] ] return epochs diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index f282447d4..c4f038103 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -51,10 +51,10 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: if not is_exchange_known_ccxt(exchange): raise OperationalException( - f'Exchange "{exchange}" is not known to the ccxt library ' - f'and therefore not available for the bot.\n' - f'The following exchanges are available for Freqtrade: ' - f'{", ".join(available_exchanges())}' + f'Exchange "{exchange}" is not known to the ccxt library ' + f'and therefore not available for the bot.\n' + f'The following exchanges are available for Freqtrade: ' + f'{", ".join(available_exchanges())}' ) valid, reason = validate_exchange(exchange) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index aad03e983..85ff4408f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -115,7 +115,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: raise OperationalException( 'The config stoploss needs to be different from 0 to avoid problems with sell orders.' - ) + ) # Skip if trailing stoploss is not activated if not conf.get('trailing_stop', False): return @@ -180,7 +180,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None: raise OperationalException( "Protections must specify either `stop_duration` or `stop_duration_candles`.\n" f"Please fix the protection {prot.get('method')}" - ) + ) if ('lookback_period' in prot and 'lookback_period_candles' in prot): raise OperationalException( diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 1b162f7c9..5efe26bd2 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -108,5 +108,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: raise OperationalException( "Both 'timeframe' and 'ticker_interval' detected." "Please remove 'ticker_interval' from your configuration to continue operating." - ) + ) config['timeframe'] = config['ticker_interval'] + + if 'protections' in config: + logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.") diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b48644c58..de4bc99b4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -280,7 +280,7 @@ CONF_SCHEMA = { 'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS, 'default': 'off' - }, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 977b7e4ec..243043d31 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -231,12 +231,12 @@ class Edge: 'Minimum expectancy and minimum winrate are met only for %s,' ' so other pairs are filtered out.', self._final_pairs - ) + ) else: logger.info( 'Edge removed all pairs as no pair with minimum expectancy ' 'and minimum winrate was found !' - ) + ) return self._final_pairs @@ -247,7 +247,7 @@ class Edge: final = [] for pair, info in self._cached_pairs.items(): 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({ 'Pair': pair, 'Winrate': info.winrate, diff --git a/freqtrade/main.py b/freqtrade/main.py index 84d4b24f8..2fd3d32bb 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -44,7 +44,7 @@ def main(sysargv: List[str] = None) -> None: "as `freqtrade trade [options...]`.\n" "To see the full list of options available, please use " "`freqtrade --help` or `freqtrade --help`." - ) + ) except SystemExit as e: return_code = e diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 45e60e013..3079e326d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -146,6 +146,8 @@ class Backtesting: # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + + def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): conf = self.config if hasattr(strategy, 'protections'): @@ -194,6 +196,7 @@ class Backtesting: Trade.reset_trades() self.rejected_trades = 0 self.dataprovider.clear_cache() + self._load_protections(self.strategy) def check_abort(self): """ diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a69e5a5a2..0db78aa39 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -66,6 +66,7 @@ class Hyperopt: def __init__(self, config: Dict[str, Any]) -> None: self.buy_space: List[Dimension] = [] self.sell_space: List[Dimension] = [] + self.protection_space: List[Dimension] = [] self.roi_space: List[Dimension] = [] self.stoploss_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} if HyperoptTools.has_space(self.config, 'sell'): 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'): result['roi'] = {str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()} @@ -241,6 +244,12 @@ class Hyperopt: """ 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'): logger.debug("Hyperopt has 'buy' space") @@ -261,8 +270,8 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'trailing'): logger.debug("Hyperopt has 'trailing' space") self.trailing_space = self.custom_hyperopt.trailing_space() - self.dimensions = (self.buy_space + self.sell_space + self.roi_space + - self.stoploss_space + self.trailing_space) + self.dimensions = (self.buy_space + self.sell_space + self.protection_space + + self.roi_space + self.stoploss_space + self.trailing_space) 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.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'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) @@ -444,9 +459,9 @@ class Hyperopt: ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', ] with progressbar.ProgressBar( - max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, - widgets=widgets - ) as pbar: + max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, + widgets=widgets + ) as pbar: EVALS = ceil(self.total_epochs / jobs) for i in range(EVALS): # Correct the number of epochs to be processed for the last diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index f86204406..03f7dd21e 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -73,6 +73,9 @@ class HyperOptAuto(IHyperOpt): def sell_indicator_space(self) -> List['Dimension']: 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]: return self._get_func('generate_roi_table')(params) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 889854cad..500798627 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -57,6 +57,13 @@ class IHyperOpt(ABC): """ 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]: """ Create an indicator space. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 439016c14..52aa85c84 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -82,8 +82,8 @@ class HyperoptTools(): """ Tell if the space value is contained in the configuration """ - # The 'trailing' space is not included in the 'default' set of spaces - if space == 'trailing': + # 'trailing' and 'protection spaces are not included in the 'default' set of spaces + if space in ('trailing', 'protection'): return any(s in config['spaces'] for s in [space, 'all']) else: return any(s in config['spaces'] for s in [space, 'all', 'default']) @@ -149,7 +149,7 @@ class HyperoptTools(): if print_json: 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) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) @@ -158,6 +158,8 @@ class HyperoptTools(): non_optimized) HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:", 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, 'stoploss', "Stoploss:", non_optimized) HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized) @@ -203,7 +205,7 @@ class HyperoptTools(): elif space == "roi": result = result[:-1] + f'{appendix}\n' 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) result += f"minimal_roi = {minimal_roi_result}" elif space == "trailing": diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index eefacbbab..7bb60228a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N filename = Path.joinpath( recordfilename.parent, 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) 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(): tabular_data.append(_generate_result_line( results['results'], results['config']['dry_run_wallet'], strategy) - ) + ) try: max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], value_col='profit_ratio') @@ -604,7 +604,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency']) stake_amount = round_coin_value( 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. " f"Your starting balance was {start_balance}, " diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 061460975..2fbf343ce 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -334,8 +334,8 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: ) elif indicator_b not in data: logger.info( - 'fill_to: "%s" ignored. Reason: This indicator is not ' - 'in your strategy.', indicator_b + 'fill_to: "%s" ignored. Reason: This indicator is not ' + 'in your strategy.', indicator_b ) return fig diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 74348b1a7..bfde2ace0 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -144,7 +144,7 @@ class IPairList(LoggingMixin, ABC): markets = self._exchange.markets if not markets: 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] = [] for pair in pairlist: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index d6b8aaaa3..901fde2d0 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -120,9 +120,9 @@ class VolumePairList(IPairList): # Use fresh pairlist # Check if pair quote currency equals to the stake currency. filtered_tickers = [ - v for k, v in tickers.items() - if (self._exchange.get_pair_quote_currency(k) == self._stake_currency - and v[self._sort_key] is not None)] + v for k, v in tickers.items() + if (self._exchange.get_pair_quote_currency(k) == self._stake_currency + and v[self._sort_key] is not None)] pairlist = [s['symbol'] for s in filtered_tickers] pairlist = self.filter_pairlist(pairlist, tickers) @@ -197,7 +197,7 @@ class VolumePairList(IPairList): if self._min_value > 0: 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]) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 03f4760b8..face79729 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -28,13 +28,13 @@ class PairListManager(): self._tickers_needed = False for pairlist_handler_config in self._config.get('pairlists', None): pairlist_handler = PairListResolver.load_pairlist( - pairlist_handler_config['method'], - exchange=exchange, - pairlistmanager=self, - config=config, - pairlistconfig=pairlist_handler_config, - pairlist_pos=len(self._pairlist_handlers) - ) + pairlist_handler_config['method'], + exchange=exchange, + pairlistmanager=self, + config=config, + pairlistconfig=pairlist_handler_config, + pairlist_pos=len(self._pairlist_handlers) + ) self._tickers_needed |= pairlist_handler.needstickers self._pairlist_handlers.append(pairlist_handler) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index d034beefc..e0a89e334 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = 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']) 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) else: self._stop_duration_candles = None self._stop_duration = protection_config.get('stop_duration', 60) 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 else: 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) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 45d393411..40edf1204 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -54,9 +54,9 @@ class StoplossGuard(IProtection): 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 ( - SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, - SellType.STOPLOSS_ON_EXCHANGE.value) - and trade.close_profit and trade.close_profit < 0)] + SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, + SellType.STOPLOSS_ON_EXCHANGE.value) + and trade.close_profit and trade.close_profit < 0)] if len(trades) < self._trade_limit: return False, None, None diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index ef24bf481..2f70a788a 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.pairlist_resolver import PairListResolver from freqtrade.resolvers.protection_resolver import ProtectionResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver - - - diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 1239b78b3..e7c077e84 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -50,7 +50,7 @@ class StrategyResolver(IResolver): if 'timeframe' not in config: logger.warning( "DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'." - ) + ) strategy.timeframe = strategy.ticker_interval if strategy._ft_params_from_file: @@ -119,7 +119,7 @@ class StrategyResolver(IResolver): - default (if not None) """ 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 setattr(strategy, attribute, config[attribute]) logger.info("Override strategy '%s' with value in config file: %s.", diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 61d69707e..f2361fda8 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -199,8 +199,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, config=Depends(get_config)): config = deepcopy(config) config.update({ - 'strategy': strategy, - }) + 'strategy': strategy, + }) return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 199e6a7db..cdc09b437 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -62,7 +62,7 @@ class CryptoToFiatConverter: # If the request is not a 429 error we want to raise the normal error logger.error( "Could not load FIAT Cryptocurrency map for the following problem: {}".format( - request_exception + request_exception ) ) except (Exception) as exception: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 67842e849..8085ece94 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -15,6 +15,7 @@ class RPCManager: """ Class to manage RPC objects (Telegram, API, ...) """ + def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules """ self.registered_modules: List[RPCHandler] = [] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a1f6a7e33..a988d2b60 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -77,7 +77,6 @@ class Telegram(RPCHandler): """ This class handles all telegram communication """ def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: - """ Init the Telegram call, and init the super class RPCHandler :param rpc: instance of RPC Helper class @@ -270,7 +269,7 @@ class Telegram(RPCHandler): noti = '' if msg_type == RPCMessageType.SELL: 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 if isinstance(sell_noti, str): noti = sell_noti @@ -278,7 +277,7 @@ class Telegram(RPCHandler): noti = sell_noti.get(str(msg['sell_reason']), default_noti) else: 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': logger.info(f"Notification '{msg_type}' not sent.") @@ -541,7 +540,7 @@ class Telegram(RPCHandler): f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" - ) + ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") @@ -576,13 +575,14 @@ class Telegram(RPCHandler): sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] - ) + ) durations = stats['durations'] - duration_msg = tabulate([ - ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], - ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] + duration_msg = tabulate( + [ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] ], headers=['', 'Avg. Duration'] ) @@ -1100,7 +1100,7 @@ class Telegram(RPCHandler): if reload_able: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)], - ]) + ]) else: reply_markup = InlineKeyboardMarkup([[]]) msg += "\nUpdated: {}".format(datetime.now().ctime()) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index bd49165df..be655fc33 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,7 +1,7 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter, - RealParameter) +from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, + IntParameter, RealParameter) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index b067e19d5..dad282d7e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -270,6 +270,28 @@ class CategoricalParameter(BaseParameter): 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): """ 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.ft_buy_params: List[BaseParameter] = [] self.ft_sell_params: List[BaseParameter] = [] + self.ft_protection_params: List[BaseParameter] = [] self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) @@ -292,11 +315,12 @@ class HyperStrategyMixin(object): :param category: :return: """ - if category not in ('buy', 'sell', None): - raise OperationalException('Category must be one of: "buy", "sell", None.') + if category not in ('buy', 'sell', 'protection', None): + raise OperationalException( + 'Category must be one of: "buy", "sell", "protection", 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: params = getattr(self, f"ft_{category}_params") @@ -324,9 +348,10 @@ class HyperStrategyMixin(object): params: Dict = { 'buy': list(cls.detect_parameters('buy')), 'sell': list(cls.detect_parameters('sell')), + 'protection': list(cls.detect_parameters('protection')), } params.update({ - 'count': len(params['buy'] + params['sell']) + 'count': len(params['buy'] + params['sell'] + params['protection']) }) return params @@ -340,9 +365,12 @@ class HyperStrategyMixin(object): self._ft_params_from_file = 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', {})) + protection_params = deep_merge_dicts(params.get('protection', {}), + getattr(self, 'protection_params', {})) self._load_params(buy_params, 'buy', hyperopt) self._load_params(sell_params, 'sell', hyperopt) + self._load_params(protection_params, 'protection', hyperopt) def load_params_from_file(self) -> Dict: filename_str = getattr(self, '__file__', '') @@ -397,7 +425,8 @@ class HyperStrategyMixin(object): """ params = { 'buy': {}, - 'sell': {} + 'sell': {}, + 'protection': {}, } for name, p in self.enumerate_parameters(): if not p.optimize or not p.in_space: diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 22b6f0be5..e089ebf31 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -38,7 +38,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') - ) + ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 13fc0853a..06d7cbc5c 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -6,8 +6,8 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy import IStrategy -from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) # -------------------------------- # Add your lib to import here diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 282b2f8e2..574819949 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -6,8 +6,8 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy import IStrategy -from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) # -------------------------------- # Add your lib to import here diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 48a087dec..19d82c454 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -6,8 +6,8 @@ */ "stake_currency": "BTC", "stake_amount": 0.05, - "fiat_display_currency": "USD", // C++-style comment - "amount_reserve_percent" : 0.05, // And more, tabs before this comment + "fiat_display_currency": "USD", // C++-style comment + "amount_reserve_percent": 0.05, // And more, tabs before this comment "dry_run": false, "timeframe": "5m", "trailing_stop": false, @@ -15,15 +15,15 @@ "trailing_stop_positive_offset": 0.0051, "trailing_only_offset_is_reached": false, "minimal_roi": { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 }, "stoploss": -0.10, "unfilledtimeout": { "buy": 10, - "sell": 30, // Trailing comma should also be accepted now + "sell": 30, // Trailing comma should also be accepted now }, "bid_strategy": { "use_order_book": false, @@ -34,7 +34,7 @@ "bids_to_ask_delta": 1 } }, - "ask_strategy":{ + "ask_strategy": { "use_order_book": false, "order_book_min": 1, "order_book_max": 9 @@ -64,7 +64,9 @@ "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "", - "ccxt_config": {"enableRateLimit": true}, + "ccxt_config": { + "enableRateLimit": true + }, "ccxt_async_config": { "enableRateLimit": false, "rateLimit": 500, @@ -103,8 +105,8 @@ "remove_pumps": false }, "telegram": { -// We can now comment out some settings -// "enabled": true, + // We can now comment out some settings + // "enabled": true, "enabled": false, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id" @@ -124,4 +126,4 @@ }, "strategy": "DefaultStrategy", "strategy_path": "user_data/strategies/" -} +} \ No newline at end of file diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 14fea573f..d146e84f1 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -399,7 +399,7 @@ def test_hyperopt_format_results(hyperopt): 'rejected_signals': 2, 'backtest_start_time': 1619718665, 'backtest_end_time': 1619718665, - } + } results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, Arrow(2017, 11, 14, 19, 32, 00), 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, "50.0": 0.01, "110.0": 0}, + 'protection': {}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, 'sell-fastd-enabled': True, @@ -592,7 +593,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, - 'params_not_optimized': {'buy': {}, 'sell': {}}, + 'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}}, 'results_metrics': ANY, 'total_profit': 3.1e-08 } @@ -1002,6 +1003,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['all'] }) hyperopt = Hyperopt(hyperopt_conf) 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.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 assert isinstance(buy_rsi_range, range) # Range from 0 - 50 (inclusive) assert len(list(buy_rsi_range)) == 51 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(): diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 9ec47dade..c0a9ae72a 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -93,7 +93,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, - )) + )) assert not freqtrade.protections.global_stop() 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( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, profit_rate=0.9, - )) + )) assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.global_stop() diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 5174f9416..9fb1122f5 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -139,9 +139,9 @@ def test_fiat_too_many_requests_response(mocker, caplog): assert length_cryptomap == 0 assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert log_has( - 'Too many requests for Coingecko API, backing off and trying again later.', - caplog - ) + 'Too many requests for Coingecko API, backing off and trying again later.', + caplog + ) def test_fiat_invalid_response(mocker, caplog): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 68f23e0fd..1517b6fcc 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -942,7 +942,7 @@ def test_api_whitelist(botclient): "whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "length": 4, "method": ["StaticPairList"] - } + } def test_api_forcebuy(botclient, mocker, fee): @@ -1033,7 +1033,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', - } + } def test_api_forcesell(botclient, mocker, ticker, fee, markets): @@ -1215,7 +1215,7 @@ def test_api_strategies(botclient): 'DefaultStrategy', 'HyperoptableStrategy', 'TestStrategyLegacy' - ]} + ]} def test_api_strategy(botclient): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index cc4734e13..88bdd078e 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -4,7 +4,8 @@ import talib.abstract as ta from pandas import DataFrame 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): @@ -64,6 +65,18 @@ class HyperoptableStrategy(IStrategy): 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', 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): """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d8c87506c..0ad6d6f32 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -16,8 +16,8 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, - IntParameter, RealParameter) +from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter, + DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -717,6 +717,17 @@ def test_hyperopt_parameters(): assert len(list(catpar.range)) == 3 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): default_conf.update({'strategy': 'HyperoptableStrategy'}) @@ -734,7 +745,8 @@ def test_auto_hyperopt_interface(default_conf): assert isinstance(all_params, dict) assert len(all_params['buy']) == 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') diff --git a/tests/test_arguments.py b/tests/test_arguments.py index fd6f162fd..5374881fa 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -125,7 +125,7 @@ def test_parse_args_backtesting_custom() -> None: '--strategy-list', 'DefaultStrategy', 'SampleStrategy' - ] + ] call_args = Arguments(args).get_parsed_arg() assert call_args['config'] == ['test_conf.json'] assert call_args['verbosity'] == 0 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7012333e9..7c555a39e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1130,17 +1130,17 @@ def test_pairlist_resolving_fallback(mocker): @pytest.mark.parametrize("setting", [ - ("ask_strategy", "use_sell_signal", True, - None, "use_sell_signal", False), - ("ask_strategy", "sell_profit_only", True, - None, "sell_profit_only", False), - ("ask_strategy", "sell_profit_offset", 0.1, - None, "sell_profit_offset", 0.01), - ("ask_strategy", "ignore_roi_if_buy_signal", True, - None, "ignore_roi_if_buy_signal", False), - ("ask_strategy", "ignore_buying_expired_candle_after", 5, - None, "ignore_buying_expired_candle_after", 6), - ]) + ("ask_strategy", "use_sell_signal", True, + None, "use_sell_signal", False), + ("ask_strategy", "sell_profit_only", True, + None, "sell_profit_only", False), + ("ask_strategy", "sell_profit_offset", 0.1, + None, "sell_profit_offset", 0.01), + ("ask_strategy", "ignore_roi_if_buy_signal", True, + None, "ignore_roi_if_buy_signal", False), + ("ask_strategy", "ignore_buying_expired_candle_after", 5, + None, "ignore_buying_expired_candle_after", 6), +]) def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): 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", [ - ("experimental", "use_sell_signal", False), - ("experimental", "sell_profit_only", True), - ("experimental", "ignore_roi_if_buy_signal", True), - ]) + ("experimental", "use_sell_signal", False), + ("experimental", "sell_profit_only", True), + ("experimental", "ignore_roi_if_buy_signal", True), +]) def test_process_removed_settings(mocker, default_conf, setting): patched_configuration_load_config_file(mocker, default_conf) @@ -1330,7 +1330,7 @@ def test_process_removed_setting(mocker, default_conf, caplog): '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." config = deepcopy(default_conf) process_temporary_deprecated_settings(config) @@ -1352,6 +1352,17 @@ def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): 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): test_args = {