Merge branch 'develop' into agefilter-max-days-listed
This commit is contained in:
commit
0f3d34eaf4
@ -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.
|
||||||
|
|
||||||
|
@ -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).
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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).
|
||||||
|
@ -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",
|
||||||
|
@ -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).',
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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"])
|
||||||
|
@ -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'},
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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': {}
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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((
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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']}: "
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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': {
|
||||||
|
@ -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
|
|
||||||
}"""
|
|
||||||
|
317
tests/optimize/test_hyperopt_tools.py
Normal file
317
tests/optimize/test_hyperopt_tools.py
Normal 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)
|
@ -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")
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user