Merge branch 'develop' into agefilter-max-days-listed

This commit is contained in:
Kevin Julian 2021-07-06 19:47:18 +07:00 committed by GitHub
commit 0f3d34eaf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 788 additions and 233 deletions

View File

@ -32,6 +32,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame], config: Dict, processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
@ -53,7 +54,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
Currently, the arguments are: Currently, the arguments are:
* `results`: DataFrame containing the result * `results`: DataFrame containing the resulting trades.
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs` `pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
* `trade_count`: Amount of trades (identical to `len(results)`) * `trade_count`: Amount of trades (identical to `len(results)`)
@ -61,6 +62,7 @@ Currently, the arguments are:
* `min_date`: End date of the timerange used * `min_date`: End date of the timerange used
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space). * `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting. * `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.

View File

@ -302,7 +302,6 @@ A backtesting result will look like that:
| Days win/draw/lose | 12 / 82 / 25 | | Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 | | Rejected Buy signals | 3089 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
@ -390,7 +389,6 @@ It contains some useful key metrics about performance of your strategy on backte
| Days win/draw/lose | 12 / 82 / 25 | | Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Zero Duration Trades | 4.6% (20) |
| Rejected Buy signals | 3089 | | Rejected Buy signals | 3089 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
@ -420,7 +418,6 @@ It contains some useful key metrics about performance of your strategy on backte
- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results.
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).

View File

@ -51,7 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--hyperopt-loss NAME] [--disable-param-export]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -118,6 +118,8 @@ optional arguments:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily, SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily SortinoHyperOptLoss, SortinoHyperOptLossDaily
--disable-param-export
Disable automatic hyperopt parameter export.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -403,6 +405,9 @@ While this strategy is most likely too simple to provide consistent profit, it s
!!! Note !!! Note
`self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`). `self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`).
!!! Note
`range` property may also be used with `DecimalParameter` and `CategoricalParameter`. `RealParameter` does not provide this property due to infinite search space.
??? Hint "Performance tip" ??? Hint "Performance tip"
By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter. By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter.
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).
@ -509,7 +514,13 @@ You should understand this result like:
* You should not use ADX because `'buy_adx_enabled': False`. * You should not use ADX because `'buy_adx_enabled': False`.
* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`) * You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`)
Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. ### Automatic parameter application to the strategy
When using Hyperoptable parameters, the result of your hyperopt-run will be written to a json file next to your strategy (so for `MyAwesomeStrategy.py`, the file would be `MyAwesomeStrategy.json`).
This file is also updated when using the `hyperopt-show` sub-command, unless `--disable-param-export` is provided to either of the 2 commands.
Your strategy class can also contain these results explicitly. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed.
Transferring your whole hyperopt result to your strategy would then look like: Transferring your whole hyperopt result to your strategy would then look like:
@ -525,6 +536,10 @@ class MyAwesomeStrategy(IStrategy):
} }
``` ```
!!! Note
Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy.
The prevalence is therefore: config > parameter file > strategy
### Understand Hyperopt ROI results ### Understand Hyperopt ROI results
If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table:

View File

@ -55,7 +55,7 @@ class AwesomeStrategy(IStrategy):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
# Obtain last available candle. Do not use current_time to look up latest candle, because # Obtain last available candle. Do not use current_time to look up latest candle, because
# current_time points to curret incomplete candle whose data is not available. # current_time points to current incomplete candle whose data is not available.
last_candle = dataframe.iloc[-1].squeeze() last_candle = dataframe.iloc[-1].squeeze()
# <...> # <...>
@ -83,7 +83,7 @@ It is possible to define custom sell signals, indicating that specified position
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
!!! Note !!! Note
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
@ -243,7 +243,7 @@ class AwesomeStrategy(IStrategy):
current_rate: float, current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
if current_profit < 0.04: if current_profit < 0.04:
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
# After reaching the desired offset, allow the stoploss to trail by half the profit # After reaching the desired offset, allow the stoploss to trail by half the profit
desired_stoploss = current_profit / 2 desired_stoploss = current_profit / 2

View File

@ -130,6 +130,39 @@ trades = load_backtest_data(backtest_dir)
trades.groupby("pair")["sell_reason"].value_counts() trades.groupby("pair")["sell_reason"].value_counts()
``` ```
## Plotting daily profit / equity line
```python
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
from freqtrade.configuration import Configuration
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
import plotly.express as px
import pandas as pd
# strategy = 'SampleStrategy'
# config = Configuration.from_files(["user_data/config.json"])
# backtest_dir = config["user_data_dir"] / "backtest_results"
stats = load_backtest_stats(backtest_dir)
strategy_stats = stats['strategy'][strategy]
equity = 0
equity_daily = []
for dp in strategy_stats['daily_profit']:
equity_daily.append(equity)
equity += float(dp)
dates = pd.date_range(strategy_stats['backtest_start'], strategy_stats['backtest_end'])
df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})
fig = px.line(df, x="dates", y="equity_daily")
fig.show()
```
### Load live trading results into a pandas dataframe ### Load live trading results into a pandas dataframe
In case you did already some trading and want to analyze your performance In case you did already some trading and want to analyze your performance

View File

@ -702,7 +702,8 @@ You can show the details of any hyperoptimization epoch previously evaluated by
usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--best] [-d PATH] [--userdir PATH] [--best]
[--profitable] [-n INT] [--print-json] [--profitable] [-n INT] [--print-json]
[--hyperopt-filename PATH] [--no-header] [--hyperopt-filename FILENAME] [--no-header]
[--disable-param-export]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -714,6 +715,8 @@ optional arguments:
Hyperopt result filename.Example: `--hyperopt- Hyperopt result filename.Example: `--hyperopt-
filename=hyperopt_results_2020-09-27_16-20-48.pickle` filename=hyperopt_results_2020-09-27_16-20-48.pickle`
--no-header Do not print epoch details header. --no-header Do not print epoch details header.
--disable-param-export
Disable automatic hyperopt parameter export.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -29,7 +29,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"epochs", "spaces", "print_all", "epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs", "print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades", "hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_loss"] "hyperopt_loss", "disableparamexport"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
@ -85,7 +85,8 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
"hyperoptexportfilename", "export_csv"] "hyperoptexportfilename", "export_csv"]
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header"] "print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
"disableparamexport"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data", "list-markets", "list-pairs", "list-strategies", "list-data",

View File

@ -178,6 +178,11 @@ AVAILABLE_CLI_OPTIONS = {
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH', metavar='PATH',
), ),
"disableparamexport": Arg(
'--disable-param-export',
help="Disable automatic hyperopt parameter export.",
action='store_true',
),
"fee": Arg( "fee": Arg(
'--fee', '--fee',
help='Specify fee ratio. Will be applied twice (on trade entry and exit).', help='Specify fee ratio. Will be applied twice (on trade entry and exit).',

View File

@ -129,9 +129,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
metrics = val['results_metrics'] metrics = val['results_metrics']
if 'strategy_name' in metrics: if 'strategy_name' in metrics:
show_backtest_result(metrics['strategy_name'], metrics, strategy_name = metrics['strategy_name']
show_backtest_result(strategy_name, metrics,
metrics['stake_currency']) metrics['stake_currency'])
HyperoptTools.try_export_params(config, strategy_name, val)
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details") header_str="Epoch details")

View File

@ -260,6 +260,8 @@ class Configuration:
self._args_to_config(config, argname='export', self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...') logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='disableparamexport',
logstring='Parameter --disableparamexport detected: {} ...')
# Edge section: # Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]: if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"]) txt_range = eval(self.args["stoploss_range"])

View File

@ -40,6 +40,7 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
LAST_BT_RESULT_FN = '.last_result.json' LAST_BT_RESULT_FN = '.last_result.json'
FTHYPT_FILEVERSION = 'fthypt_fileversion'
USERPATH_HYPEROPTS = 'hyperopts' USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies' USERPATH_STRATEGIES = 'strategies'
@ -312,6 +313,7 @@ CONF_SCHEMA = {
}, },
'db_url': {'type': 'string'}, 'db_url': {'type': 'string'},
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'}, 'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
'disableparamexport': {'type': 'boolean'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'forcebuy_enable': {'type': 'boolean'}, 'forcebuy_enable': {'type': 'boolean'},
'disable_dataframe_checks': {'type': 'boolean'}, 'disable_dataframe_checks': {'type': 'boolean'},

View File

@ -194,8 +194,8 @@ def _download_pair_history(datadir: Path,
new_data = exchange.get_historic_ohlcv(pair=pair, new_data = exchange.get_historic_ohlcv(pair=pair,
timeframe=timeframe, timeframe=timeframe,
since_ms=since_ms if since_ms else since_ms=since_ms if since_ms else
int(arrow.utcnow().shift( arrow.utcnow().shift(
days=-new_pairs_days).float_timestamp) * 1000 days=-new_pairs_days).int_timestamp * 1000
) )
# TODO: Maybe move parsing to exchange class (?) # TODO: Maybe move parsing to exchange class (?)
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
@ -272,7 +272,7 @@ def _download_trades_history(exchange: Exchange,
if timerange.stoptype == 'date': if timerange.stoptype == 'date':
until = timerange.stopts * 1000 until = timerange.stopts * 1000
else: else:
since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000 since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000
trades = data_handler.trades_load(pair) trades = data_handler.trades_load(pair)

View File

@ -578,7 +578,7 @@ class Exchange:
'side': side, 'side': side,
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': int(arrow.utcnow().int_timestamp * 1000), 'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, 'fee': None,
'info': {} 'info': {}

View File

@ -12,7 +12,6 @@ from math import ceil
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import numpy as np
import progressbar import progressbar
import rapidjson import rapidjson
from colorama import Fore, Style from colorama import Fore, Style
@ -20,16 +19,16 @@ from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
from freqtrade.data.converter import trim_dataframes from freqtrade.data.converter import trim_dataframes
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.misc import file_dump_json, plural from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
@ -78,8 +77,11 @@ class Hyperopt:
if not self.config.get('hyperopt'): if not self.config.get('hyperopt'):
self.custom_hyperopt = HyperOptAuto(self.config) self.custom_hyperopt = HyperOptAuto(self.config)
self.auto_hyperopt = True
else: else:
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.auto_hyperopt = False
self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperopt.strategy = self.backtesting.strategy
@ -163,13 +165,9 @@ class Hyperopt:
While not a valid json object - this allows appending easily. While not a valid json object - this allows appending easily.
:param epoch: result dictionary for this epoch. :param epoch: result dictionary for this epoch.
""" """
def default_parser(x): epoch[FTHYPT_FILEVERSION] = 2
if isinstance(x, np.integer):
return int(x)
return str(x)
with self.results_file.open('a') as f: with self.results_file.open('a') as f:
rapidjson.dump(epoch, f, default=default_parser, rapidjson.dump(epoch, f, default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
f.write("\n") f.write("\n")
@ -201,6 +199,25 @@ class Hyperopt:
return result return result
def _get_no_optimize_details(self) -> Dict[str, Any]:
"""
Get non-optimized parameters
"""
result: Dict[str, Any] = {}
strategy = self.backtesting.strategy
if not HyperoptTools.has_space(self.config, 'roi'):
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()}
if not HyperoptTools.has_space(self.config, 'stoploss'):
result['stoploss'] = {'stoploss': strategy.stoploss}
if not HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = {
'trailing_stop': strategy.trailing_stop,
'trailing_stop_positive': strategy.trailing_stop_positive,
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
}
return result
def print_results(self, results) -> None: def print_results(self, results) -> None:
""" """
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
@ -310,7 +327,8 @@ class Hyperopt:
results_explanation = HyperoptTools.format_results_explanation_string( results_explanation = HyperoptTools.format_results_explanation_string(
strat_stats, self.config['stake_currency']) strat_stats, self.config['stake_currency'])
not_optimized = self.backtesting.strategy.get_params_dict() not_optimized = self.backtesting.strategy.get_no_optimize_params()
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
trade_count = strat_stats['total_trades'] trade_count = strat_stats['total_trades']
total_profit = strat_stats['profit_total'] total_profit = strat_stats['profit_total']
@ -324,7 +342,8 @@ class Hyperopt:
loss = self.calculate_loss(results=backtesting_results['results'], loss = self.calculate_loss(results=backtesting_results['results'],
trade_count=trade_count, trade_count=trade_count,
min_date=min_date, max_date=max_date, min_date=min_date, max_date=max_date,
config=self.config, processed=processed) config=self.config, processed=processed,
backtest_stats=strat_stats)
return { return {
'loss': loss, 'loss': loss,
'params_dict': params_dict, 'params_dict': params_dict,
@ -469,6 +488,12 @@ class Hyperopt:
f"saved to '{self.results_file}'.") f"saved to '{self.results_file}'.")
if self.current_best_epoch: if self.current_best_epoch:
if self.auto_hyperopt:
HyperoptTools.try_export_params(
self.config,
self.backtesting.strategy.get_strategy_name(),
self.current_best_epoch)
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs, HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
self.print_json) self.print_json)
else: else:

View File

@ -5,7 +5,7 @@ This module defines the interface for the loss-function for hyperopt
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Any, Dict
from pandas import DataFrame from pandas import DataFrame
@ -22,6 +22,7 @@ class IHyperOptLoss(ABC):
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame], config: Dict, processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results

View File

@ -1,23 +1,82 @@
import io import io
import logging import logging
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List, Optional
import numpy as np
import rapidjson import rapidjson
import tabulate import tabulate
from colorama import Fore, Style from colorama import Fore, Style
from pandas import isna, json_normalize from pandas import isna, json_normalize
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value, round_dict from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
def hyperopt_serializer(x):
if isinstance(x, np.integer):
return int(x)
if isinstance(x, np.bool_):
return bool(x)
return str(x)
class HyperoptTools(): class HyperoptTools():
@staticmethod
def get_strategy_filename(config: Dict, strategy_name: str) -> Optional[Path]:
"""
Get Strategy-location (filename) from strategy_name
"""
from freqtrade.resolvers.strategy_resolver import StrategyResolver
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
strategy_objs = StrategyResolver.search_all_objects(directory, False)
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
if strategies:
strategy = strategies[0]
return Path(strategy['location'])
return None
@staticmethod
def export_params(params, strategy_name: str, filename: Path):
"""
Generate files
"""
final_params = deepcopy(params['params_not_optimized'])
final_params = deep_merge_dicts(params['params_details'], final_params)
final_params = {
'strategy_name': strategy_name,
'params': final_params,
'ft_stratparam_v': 1,
'export_time': datetime.now(timezone.utc),
}
logger.info(f"Dumping parameters to {filename}")
rapidjson.dump(final_params, filename.open('w'), indent=2,
default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
)
@staticmethod
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
# Export parameters ...
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
if fn:
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
else:
logger.warn("Strategy not found, not exporting parameter file.")
@staticmethod @staticmethod
def has_space(config: Dict[str, Any], space: str) -> bool: def has_space(config: Dict[str, Any], space: str) -> bool:
""" """
@ -99,9 +158,9 @@ 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, 'roi', "ROI table:") HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
@staticmethod @staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@ -127,23 +186,34 @@ class HyperoptTools():
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
if space in params or space in non_optimized: if space in params or space in non_optimized:
space_params = HyperoptTools._space_params(params, space, 5) space_params = HyperoptTools._space_params(params, space, 5)
no_params = HyperoptTools._space_params(non_optimized, space, 5)
appendix = ''
if not space_params and not no_params:
# No parameters - don't print
return
if not space_params:
# Not optimized parameters - append string
appendix = NON_OPT_PARAM_APPENDIX
result = f"\n# {header}\n" result = f"\n# {header}\n"
if space == 'stoploss': if space == "stoploss":
result += f"stoploss = {space_params.get('stoploss')}" stoploss = safe_value_fallback2(space_params, no_params, space, space)
elif space == 'roi': result += (f"stoploss = {stoploss}{appendix}")
elif space == "roi":
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.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":
for k, v in (space_params or no_params).items():
for k, v in space_params.items(): result += f"{k} = {v}{appendix}\n"
result += f'{k} = {v}\n'
else: else:
no_params = HyperoptTools._space_params(non_optimized, space, 5) # Buy / sell parameters
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
result = result.replace("\n", "\n ") result = result.replace("\n", "\n ")
print(result) print(result)
@ -157,7 +227,7 @@ class HyperoptTools():
return {} return {}
@staticmethod @staticmethod
def _pprint(params, non_optimized, indent: int = 4): def _pprint_dict(params, non_optimized, indent: int = 4):
""" """
Pretty-print hyperopt results (based on 2 dicts - with add. comment) Pretty-print hyperopt results (based on 2 dicts - with add. comment)
""" """
@ -169,7 +239,7 @@ class HyperoptTools():
result += " " * indent + f'"{k}": ' result += " " * indent + f'"{k}": '
result += f'"{param}",' if isinstance(param, str) else f'{param},' result += f'"{param}",' if isinstance(param, str) else f'{param},'
if k in non_optimized: if k in non_optimized:
result += " # value loaded from strategy" result += NON_OPT_PARAM_APPENDIX
result += "\n" result += "\n"
result += '}' result += '}'
return result return result

View File

@ -229,8 +229,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
winning_trades = results.loc[results['profit_ratio'] > 0] winning_trades = results.loc[results['profit_ratio'] > 0]
draw_trades = results.loc[results['profit_ratio'] == 0] draw_trades = results.loc[results['profit_ratio'] == 0]
losing_trades = results.loc[results['profit_ratio'] < 0] losing_trades = results.loc[results['profit_ratio'] < 0]
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
(results['sell_reason'] == 'trailing_stop_loss')])
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean())) holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
if not results.empty else timedelta()) if not results.empty else timedelta())
@ -249,7 +247,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
'winner_holding_avg_s': winner_holding_avg.total_seconds(), 'winner_holding_avg_s': winner_holding_avg.total_seconds(),
'loser_holding_avg': loser_holding_avg, 'loser_holding_avg': loser_holding_avg,
'loser_holding_avg_s': loser_holding_avg.total_seconds(), 'loser_holding_avg_s': loser_holding_avg.total_seconds(),
'zero_duration_trades': zero_duration_trades,
} }
@ -264,6 +261,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': 0, 'winning_days': 0,
'draw_days': 0, 'draw_days': 0,
'losing_days': 0, 'losing_days': 0,
'daily_profit_list': [],
} }
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
@ -274,6 +272,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
winning_days = sum(daily_profit > 0) winning_days = sum(daily_profit > 0)
draw_days = sum(daily_profit == 0) draw_days = sum(daily_profit == 0)
losing_days = sum(daily_profit < 0) losing_days = sum(daily_profit < 0)
daily_profit_list = daily_profit.tolist()
return { return {
'backtest_best_day': best_rel, 'backtest_best_day': best_rel,
@ -283,6 +282,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winning_days': winning_days, 'winning_days': winning_days,
'draw_days': draw_days, 'draw_days': draw_days,
'losing_days': losing_days, 'losing_days': losing_days,
'daily_profit': daily_profit_list,
} }
@ -542,14 +542,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
# command stores these results and newer version of freqtrade must be able to handle old # command stores these results and newer version of freqtrade must be able to handle old
# results with missing new fields. # results with missing new fields.
zero_duration_trades = '--'
if 'zero_duration_trades' in strat_results:
zero_duration_trades_per = \
100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades']
zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \
f'({strat_results["zero_duration_trades"]})'
metrics = [ metrics = [
('Backtesting from', strat_results['backtest_start']), ('Backtesting from', strat_results['backtest_start']),
('Backtesting to', strat_results['backtest_end']), ('Backtesting to', strat_results['backtest_end']),
@ -585,7 +577,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{strat_results['draw_days']} / {strat_results['losing_days']}"), f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Zero Duration Trades', zero_duration_trades),
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability

View File

@ -105,7 +105,7 @@ class AgeFilter(IPairList):
len(daily_candles) <= self._max_days_listed: len(daily_candles) <= self._max_days_listed:
# We have fetched at least the minimum required number of daily candles # We have fetched at least the minimum required number of daily candles
# Add to cache, store the time we last checked this symbol # Add to cache, store the time we last checked this symbol
self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000 self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000
return True return True
else: else:
self.log_once(( self.log_once((

View File

@ -69,10 +69,10 @@ class VolatilityFilter(IPairList):
""" """
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
since_ms = int(arrow.utcnow() since_ms = (arrow.utcnow()
.floor('day') .floor('day')
.shift(days=-self._days - 1) .shift(days=-self._days - 1)
.float_timestamp) * 1000 .int_timestamp) * 1000
# Get all candles # Get all candles
candles = {} candles = {}
if needed_pairs: if needed_pairs:

View File

@ -62,10 +62,10 @@ class RangeStabilityFilter(IPairList):
""" """
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
since_ms = int(arrow.utcnow() since_ms = (arrow.utcnow()
.floor('day') .floor('day')
.shift(days=-self._days - 1) .shift(days=-self._days - 1)
.float_timestamp) * 1000 .int_timestamp) * 1000
# Get all candles # Get all candles
candles = {} candles = {}
if needed_pairs: if needed_pairs:

View File

@ -53,6 +53,21 @@ class StrategyResolver(IResolver):
) )
strategy.timeframe = strategy.ticker_interval strategy.timeframe = strategy.ticker_interval
if strategy._ft_params_from_file:
# Set parameters from Hyperopt results file
params = strategy._ft_params_from_file
strategy.minimal_roi = params.get('roi', strategy.minimal_roi)
strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss)
trailing = params.get('trailing', {})
strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop)
strategy.trailing_stop_positive = trailing.get('trailing_stop_positive',
strategy.trailing_stop_positive)
strategy.trailing_stop_positive_offset = trailing.get(
'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset)
strategy.trailing_only_offset_is_reached = trailing.get(
'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached)
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
# (Attribute name, default, subkey) # (Attribute name, default, subkey)

View File

@ -18,6 +18,17 @@ async def fallback():
return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html')) return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html'))
@router_ui.get('/ui_version', include_in_schema=False)
async def ui_version():
from freqtrade.commands.deploy_commands import read_ui_version
uibase = Path(__file__).parent / 'ui/installed/'
version = read_ui_version(uibase)
return {
"version": version if version else "not_installed",
}
@router_ui.get('/{rest_of_path:path}', include_in_schema=False) @router_ui.get('/{rest_of_path:path}', include_in_schema=False)
async def index_html(rest_of_path: str): async def index_html(rest_of_path: str):
""" """

View File

@ -24,7 +24,7 @@ from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN from freqtrade.constants import DUST_PER_COIN
from freqtrade.enums import RPCMessageType from freqtrade.enums import RPCMessageType
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, round_coin_value from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc import RPC, RPCException, RPCHandler
@ -598,6 +598,9 @@ class Telegram(RPCHandler):
"Starting capital: " "Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
) )
total_dust_balance = 0
total_dust_currencies = 0
curr_output = ''
for curr in result['currencies']: for curr in result['currencies']:
if curr['est_stake'] > balance_dust_level: if curr['est_stake'] > balance_dust_level:
curr_output = ( curr_output = (
@ -607,9 +610,9 @@ class Telegram(RPCHandler):
f"\t`Pending: {curr['used']:.8f}`\n" f"\t`Pending: {curr['used']:.8f}`\n"
f"\t`Est. {curr['stake']}: " f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else: elif curr['est_stake'] <= balance_dust_level:
curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} " total_dust_balance += curr['est_stake']
f"{curr['stake']} amount \n") total_dust_currencies += 1
# Handle overflowing message length # Handle overflowing message length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -618,6 +621,14 @@ class Telegram(RPCHandler):
else: else:
output += curr_output output += curr_output
if total_dust_balance > 0:
output += (
f"*{total_dust_currencies} Other "
f"{plural(total_dust_currencies, 'Currency', 'Currencies')} "
f"(< {balance_dust_level} {result['stake']}):*\n"
f"\t`Est. {result['stake']}: "
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
output += ("\n*Estimated Value*:\n" output += ("\n*Estimated Value*:\n"
f"\t`{result['stake']}: {result['total']: .8f}`\n" f"\t`{result['stake']}: {result['total']: .8f}`\n"
f"\t`{result['symbol']}: " f"\t`{result['symbol']}: "

View File

@ -5,8 +5,10 @@ This module defines a base class for auto-hyperoptable strategies.
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import suppress from contextlib import suppress
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.hyperopt_tools import HyperoptTools
@ -205,6 +207,21 @@ class DecimalParameter(NumericParameter):
return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name, return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
**self._space_params) **self._space_params)
@property
def range(self):
"""
Get each value in this space as list.
Returns a List from low to high (inclusive) in Hyperopt mode.
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
calculating 100ds of indicators.
"""
if self.in_space and self.optimize:
low = int(self.low * pow(10, self._decimals))
high = int(self.high * pow(10, self._decimals)) + 1
return [round(n * pow(0.1, self._decimals), self._decimals) for n in range(low, high)]
else:
return [self.value]
class CategoricalParameter(BaseParameter): class CategoricalParameter(BaseParameter):
default: Any default: Any
@ -239,6 +256,19 @@ class CategoricalParameter(BaseParameter):
""" """
return Categorical(self.opt_range, name=name, **self._space_params) return Categorical(self.opt_range, name=name, **self._space_params)
@property
def range(self):
"""
Get each value in this space as list.
Returns a List of categories in Hyperopt mode.
Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid
calculating 100ds of indicators.
"""
if self.in_space and self.optimize:
return self.opt_range
else:
return [self.value]
class HyperStrategyMixin(object): class HyperStrategyMixin(object):
""" """
@ -305,10 +335,36 @@ class HyperStrategyMixin(object):
""" """
Load Hyperoptable parameters Load Hyperoptable parameters
""" """
self._load_params(getattr(self, 'buy_params', None), 'buy', hyperopt) params = self.load_params_from_file()
self._load_params(getattr(self, 'sell_params', None), 'sell', hyperopt) params = params.get('params', {})
self._ft_params_from_file = params
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', None))
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', None))
def _load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: self._load_params(buy_params, 'buy', hyperopt)
self._load_params(sell_params, 'sell', hyperopt)
def load_params_from_file(self) -> Dict:
filename_str = getattr(self, '__file__', '')
if not filename_str:
return {}
filename = Path(filename_str).with_suffix('.json')
if filename.is_file():
logger.info(f"Loading parameters from file {filename}")
try:
params = json_load(filename.open('r'))
if params.get('strategy_name') != self.__class__.__name__:
raise OperationalException('Invalid parameter file provided.')
return params
except ValueError:
logger.warning("Invalid parameter file format.")
return {}
logger.info("Found no parameter file.")
return {}
def _load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None:
""" """
Set optimizable parameter values. Set optimizable parameter values.
:param params: Dictionary with new parameter values. :param params: Dictionary with new parameter values.
@ -335,7 +391,7 @@ class HyperStrategyMixin(object):
else: else:
logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}')
def get_params_dict(self): def get_no_optimize_params(self):
""" """
Returns list of Parameters that are not part of the current optimize job Returns list of Parameters that are not part of the current optimize job
""" """

View File

@ -62,6 +62,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_populate_fun_len: int = 0 _populate_fun_len: int = 0
_buy_fun_len: int = 0 _buy_fun_len: int = 0
_sell_fun_len: int = 0 _sell_fun_len: int = 0
_ft_params_from_file: Dict = {}
# associated minimal roi # associated minimal roi
minimal_roi: Dict minimal_roi: Dict

View File

@ -188,6 +188,47 @@
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
] ]
}, },
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting daily profit / equity line"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
"\n",
"from freqtrade.configuration import Configuration\n",
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
"import plotly.express as px\n",
"import pandas as pd\n",
"\n",
"# strategy = 'SampleStrategy'\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"\n",
"stats = load_backtest_stats(backtest_dir)\n",
"strategy_stats = stats['strategy'][strategy]\n",
"\n",
"equity = 0\n",
"equity_daily = []\n",
"for dp in strategy_stats['daily_profit']:\n",
" equity_daily.append(equity)\n",
" equity += float(dp)\n",
"\n",
"dates = pd.date_range(strategy_stats['backtest_start'], strategy_stats['backtest_end'])\n",
"\n",
"df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})\n",
"\n",
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
"fig.show()\n"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
@ -329,7 +370,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.7.4" "version": "3.8.5"
}, },
"mimetype": "text/x-python", "mimetype": "text/x-python",
"name": "python", "name": "python",

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.0.0 plotly==5.1.0

View File

@ -1,12 +1,12 @@
numpy==1.21.0 numpy==1.21.0
pandas==1.2.5 pandas==1.3.0
ccxt==1.52.4 ccxt==1.52.40
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.7 cryptography==3.4.7
aiohttp==3.7.4.post0 aiohttp==3.7.4.post0
SQLAlchemy==1.4.19 SQLAlchemy==1.4.20
python-telegram-bot==13.6 python-telegram-bot==13.7
arrow==1.1.1 arrow==1.1.1
cachetools==4.2.2 cachetools==4.2.2
requests==2.25.1 requests==2.25.1
@ -31,7 +31,7 @@ python-rapidjson==1.4
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.65.2 fastapi==0.66.0
uvicorn==0.14.0 uvicorn==0.14.0
pyjwt==2.1.0 pyjwt==2.1.0
aiofiles==0.7.0 aiofiles==0.7.0

View File

@ -1168,6 +1168,7 @@ def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=saved_hyperopt_results) MagicMock(return_value=saved_hyperopt_results)
) )
mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result')
args = [ args = [
"hyperopt-show", "hyperopt-show",

View File

@ -324,6 +324,7 @@ def get_default_conf(testdatadir):
"verbosity": 3, "verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"), "strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "DefaultStrategy", "strategy": "DefaultStrategy",
"disableparamexport": True,
"internals": {}, "internals": {},
"export": "none", "export": "none",
} }
@ -1953,12 +1954,13 @@ def saved_hyperopt_results():
'params_dict': { 'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501 'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625, 'total_profit': -0.00125625,
'current_epoch': 1, 'current_epoch': 1,
'is_initial_point': True, 'is_initial_point': True,
'is_best': True 'is_best': True,
}, { }, {
'loss': 20.0, 'loss': 20.0,
'params_dict': { 'params_dict': {

View File

@ -1,9 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import logging
import re
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List
from unittest.mock import ANY, MagicMock from unittest.mock import ANY, MagicMock
import pandas as pd import pandas as pd
@ -28,12 +25,6 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
from .hyperopts.default_hyperopt import DefaultHyperOpt from .hyperopts.default_hyperopt import DefaultHyperOpt
# Functions for recurrent object patching
def create_results() -> List[Dict]:
return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -303,52 +294,6 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
assert caplog.record_tuples == [] assert caplog.record_tuples == []
def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None:
# Test writing to temp dir and reading again
epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG)
for epoch in epochs:
hyperopt._save_result(epoch)
assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
assert len(hyperopt_epochs) == 2
def test_load_previous_results(testdatadir, caplog) -> None:
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading pickled epochs from .*", caplog)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
HyperoptTools.load_previous_results(results_file)
def test_roi_table_generation(hyperopt) -> None: def test_roi_table_generation(hyperopt) -> None:
params = { params = {
'roi_t1': 5, 'roi_t1': 5,
@ -362,6 +307,18 @@ def test_roi_table_generation(hyperopt) -> None:
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_params_no_optimize_details(hyperopt) -> None:
hyperopt.config['spaces'] = ['buy']
res = hyperopt._get_no_optimize_details()
assert isinstance(res, dict)
assert "trailing" in res
assert res["trailing"]['trailing_stop'] is False
assert "roi" in res
assert res['roi']['0'] == 0.04
assert "stoploss" in res
assert res['stoploss']['stoploss'] == -0.1
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
@ -467,40 +424,6 @@ def test_hyperopt_format_results(hyperopt):
assert '0:50:00 min' in result assert '0:50:00 min' in result
@pytest.mark.parametrize("spaces, expected_results", [
(['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}),
(['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}),
(['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}),
(['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}),
(['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
])
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
def test_populate_indicators(hyperopt, testdatadir) -> None: def test_populate_indicators(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
@ -686,6 +609,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
def test_clean_hyperopt(mocker, hyperopt_conf, caplog): def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch("freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file",
MagicMock(return_value={}))
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
h = Hyperopt(hyperopt_conf) h = Hyperopt(hyperopt_conf)
@ -1068,42 +993,6 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
hyperopt.start() hyperopt.start()
def test_show_epoch_details(capsys):
test_result = {
'params_details': {
'trailing': {
'trailing_stop': True,
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.04,
'trailing_only_offset_is_reached': True
},
'roi': {
0: 0.18,
90: 0.14,
225: 0.05,
430: 0},
},
'results_explanation': 'foo result',
'is_initial_point': False,
'total_profit': 0,
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
'is_best': True
}
HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out)
assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE)
assert '# ROI table:' in captured.out
assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE)
assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE)
def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
@ -1143,17 +1032,3 @@ def test_SKDecimal():
assert space.transform([2.0]) == [200] assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100] assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160] assert space.transform([1.5, 1.6]) == [150, 160]
def test___pprint():
params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'}
non_params = {'buy_notoptimied': 55}
x = HyperoptTools._pprint(params, non_params)
assert x == """{
"buy_std": 1.2,
"buy_rsi": 31,
"buy_enable": True,
"buy_what": "asdf",
"buy_notoptimied": 55, # value loaded from strategy
}"""

View File

@ -0,0 +1,317 @@
import logging
import re
from pathlib import Path
from typing import Dict, List
import numpy as np
import pytest
import rapidjson
from freqtrade.constants import FTHYPT_FILEVERSION
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from tests.conftest import log_has, log_has_re
# Functions for recurrent object patching
def create_results() -> List[Dict]:
return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
# Test writing to temp dir and reading again
epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG)
for epoch in epochs:
hyperopt._save_result(epoch)
assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
assert len(hyperopt_epochs) == 2
def test_load_previous_results(testdatadir, caplog) -> None:
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading pickled epochs from .*", caplog)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
HyperoptTools.load_previous_results(results_file)
@pytest.mark.parametrize("spaces, expected_results", [
(['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}),
(['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}),
(['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}),
(['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}),
(['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
(['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}),
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}),
])
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
def test_show_epoch_details(capsys):
test_result = {
'params_details': {
'trailing': {
'trailing_stop': True,
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.04,
'trailing_only_offset_is_reached': True
},
'roi': {
0: 0.18,
90: 0.14,
225: 0.05,
430: 0},
},
'results_explanation': 'foo result',
'is_initial_point': False,
'total_profit': 0,
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
'is_best': True
}
HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out)
assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE)
assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE)
assert '# ROI table:' in captured.out
assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE)
assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE)
def test__pprint_dict():
params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'}
non_params = {'buy_notoptimied': 55}
x = HyperoptTools._pprint_dict(params, non_params)
assert x == """{
"buy_std": 1.2,
"buy_rsi": 31,
"buy_enable": True,
"buy_what": "asdf",
"buy_notoptimied": 55, # value loaded from strategy
}"""
def test_get_strategy_filename(default_conf):
x = HyperoptTools.get_strategy_filename(default_conf, 'DefaultStrategy')
assert isinstance(x, Path)
assert x == Path(__file__).parents[1] / 'strategy/strats/default_strategy.py'
x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy')
assert x is None
def test_export_params(tmpdir):
filename = Path(tmpdir) / "DefaultStrategy.json"
assert not filename.is_file()
params = {
"params_details": {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
"roi": {
"0": 0.528,
"346": 0.08499,
"507": 0.049,
"1595": 0
}
},
"params_not_optimized": {
"stoploss": -0.05,
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
}
}
HyperoptTools.export_params(params, "DefaultStrategy", filename)
assert filename.is_file()
content = rapidjson.load(filename.open('r'))
assert content['strategy_name'] == 'DefaultStrategy'
assert 'params' in content
assert "buy" in content["params"]
assert "sell" in content["params"]
assert "roi" in content["params"]
assert "stoploss" in content["params"]
assert "trailing" in content["params"]
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
default_conf['disableparamexport'] = False
export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params")
filename = Path(tmpdir) / "DefaultStrategy.json"
assert not filename.is_file()
params = {
"params_details": {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
"roi": {
"0": 0.528,
"346": 0.08499,
"507": 0.049,
"1595": 0
}
},
"params_not_optimized": {
"stoploss": -0.05,
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
},
FTHYPT_FILEVERSION: 2,
}
HyperoptTools.try_export_params(default_conf, "DefaultStrategy22", params)
assert log_has("Strategy not found, not exporting parameter file.", caplog)
assert export_mock.call_count == 0
caplog.clear()
HyperoptTools.try_export_params(default_conf, "DefaultStrategy", params)
assert export_mock.call_count == 1
assert export_mock.call_args_list[0][0][1] == 'DefaultStrategy'
assert export_mock.call_args_list[0][0][2].name == 'default_strategy.json'
def test_params_print(capsys):
params = {
"buy": {
"buy_rsi": 30
},
"sell": {
"sell_rsi": 70
},
}
non_optimized = {
"buy": {
"buy_adx": 44
},
"sell": {
"sell_adx": 65
},
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.05,
"20": 0.01,
},
"trailing": {
"trailing_stop": False,
"trailing_stop_positive": 0.05,
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
}
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
captured = capsys.readouterr()
assert re.search("# No header", captured.out)
assert re.search('"buy_rsi": 30,\n', captured.out)
assert re.search('"buy_adx": 44, # value loaded.*\n', captured.out)
assert not re.search("sell", captured.out)
HyperoptTools._params_pretty_print(params, 'sell', 'Sell Header', non_optimized)
captured = capsys.readouterr()
assert re.search("# Sell Header", captured.out)
assert re.search('"sell_rsi": 70,\n', captured.out)
assert re.search('"sell_adx": 65, # value loaded.*\n', captured.out)
HyperoptTools._params_pretty_print(params, 'roi', 'ROI Table:', non_optimized)
captured = capsys.readouterr()
assert re.search("# ROI Table: # value loaded.*\n", captured.out)
assert re.search('minimal_roi = {\n', captured.out)
assert re.search('"20": 0.01\n', captured.out)
HyperoptTools._params_pretty_print(params, 'trailing', 'Trailing stop:', non_optimized)
captured = capsys.readouterr()
assert re.search("# Trailing stop:", captured.out)
assert re.search('trailing_stop = False # value loaded.*\n', captured.out)
assert re.search('trailing_stop_positive = 0.05 # value loaded.*\n', captured.out)
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
def test_hyperopt_serializer():
assert isinstance(hyperopt_serializer(np.int_(5)), int)
assert isinstance(hyperopt_serializer(np.bool_(True)), bool)
assert isinstance(hyperopt_serializer(np.bool_(False)), bool)

View File

@ -105,6 +105,15 @@ def test_api_ui_fallback(botclient):
assert rc.status_code == 200 assert rc.status_code == 200
def test_api_ui_version(botclient, mocker):
ftbot, client = botclient
mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value='0.1.2')
rc = client_get(client, "/ui_version")
assert rc.status_code == 200
assert rc.json()['version'] == '0.1.2'
def test_api_auth(): def test_api_auth():
with pytest.raises(ValueError): with pytest.raises(ValueError):
create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType")

View File

@ -519,12 +519,15 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*BTC:*' in result assert '*BTC:*' in result
assert '*ETH:*' not in result assert '*ETH:*' not in result
assert '*USDT:*' in result assert '*USDT:*' not in result
assert '*EUR:*' in result assert '*EUR:*' not in result
assert '*LTC:*' in result
assert '*XRP:*' not in result
assert 'Balance:' in result assert 'Balance:' in result
assert 'Est. BTC:' in result assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result assert 'BTC: 12.00000000' in result
assert '*XRP:* not showing <0.0001 BTC amount' in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result
assert 'BTC: 0.00000309' in result
def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response(default_conf, update, mocker) -> None:

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
@ -12,6 +13,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
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, CategoricalParameter, DecimalParameter,
@ -657,17 +659,31 @@ def test_hyperopt_parameters():
assert list(intpar.range) == [0, 1, 2, 3, 4, 5] assert list(intpar.range) == [0, 1, 2, 3, 4, 5]
fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy')
assert fltpar.value == 1
assert isinstance(fltpar.get_space(''), Real) assert isinstance(fltpar.get_space(''), Real)
assert fltpar.value == 1
fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') fltpar = DecimalParameter(low=0.0, high=0.5, default=0.14, decimals=1, space='buy')
assert isinstance(fltpar.get_space(''), Integer) assert fltpar.value == 0.1
assert fltpar.value == 1 assert isinstance(fltpar.get_space(''), SKDecimal)
assert isinstance(fltpar.range, list)
assert len(list(fltpar.range)) == 1
# Range contains ONLY the default / value.
assert list(fltpar.range) == [fltpar.value]
fltpar.in_space = True
assert len(list(fltpar.range)) == 6
assert list(fltpar.range) == [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'],
default='buy_macd', space='buy') default='buy_macd', space='buy')
assert isinstance(catpar.get_space(''), Categorical)
assert catpar.value == 'buy_macd' assert catpar.value == 'buy_macd'
assert isinstance(catpar.get_space(''), Categorical)
assert isinstance(catpar.range, list)
assert len(list(catpar.range)) == 1
# Range contains ONLY the default / value.
assert list(catpar.range) == [catpar.value]
catpar.in_space = True
assert len(list(catpar.range)) == 3
assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none']
def test_auto_hyperopt_interface(default_conf): def test_auto_hyperopt_interface(default_conf):
@ -692,3 +708,50 @@ def test_auto_hyperopt_interface(default_conf):
with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
[x for x in strategy.detect_parameters('sell')] [x for x in strategy.detect_parameters('sell')]
def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
default_conf.update({'strategy': 'HyperoptableStrategy'})
del default_conf['stoploss']
del default_conf['minimal_roi']
mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
mocker.patch.object(Path, 'open')
expected_result = {
"strategy_name": "HyperoptableStrategy",
"params": {
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.2,
"1200": 0.01
}
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
PairLocks.timeframe = default_conf['timeframe']
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.stoploss == -0.05
assert strategy.minimal_roi == {0: 0.2, 1200: 0.01}
expected_result = {
"strategy_name": "HyperoptableStrategy_No",
"params": {
"stoploss": {
"stoploss": -0.05,
},
"roi": {
"0": 0.2,
"1200": 0.01
}
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
StrategyResolver.load_strategy(default_conf)
mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
StrategyResolver.load_strategy(default_conf)
assert log_has("Invalid parameter file format.", caplog)