Merge remote-tracking branch 'origin/tmp/calcprofit' into pr/mkavinkumar1/6545

This commit is contained in:
Matthias 2022-06-17 19:45:46 +02:00
commit 1250c0a32b
24 changed files with 969 additions and 129 deletions

View File

@ -22,50 +22,79 @@ DataFrame of the candles that resulted in buy signals. Depending on how many buy
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
folder to delete old exports.
To analyze the buy tags, we need to use the `buy_reasons.py` script from
[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions
in their README to copy the script into your `freqtrade/scripts/` folder.
Before running your next backtest, make sure you either delete your old backtest results or run
backtesting with the `--cache none` option to make sure no cached results are used.
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
`user_data/backtest_results` folder.
Now run the `buy_reasons.py` script, supplying a few options:
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
``` bash
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
```
The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0)
to the most detailed per pair, per buy and per sell tag (4). More options are available by
running with the `-h` option.
This command will read from the last backtesting results. The `--analysis-groups` option is
used to specify the various tabular outputs showing the profit fo each group or trade,
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
* 1: profit summaries grouped by enter_tag
* 2: profit summaries grouped by enter_tag and exit_tag
* 3: profit summaries grouped by pair and enter_tag
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
More options are available by running with the `-h` option.
### Using export-filename
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
back to a previous backtest output, you need to supply the `--export-filename` option.
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
output file. This allows you to keep historical versions of backtest results and re-analyse
them at a later date:
``` bash
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json
```
You should see some output similar to below in the logs with the name of the timestamped
filename that was exported:
```
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
```
You can then use that filename in `backtesting-analysis`:
```
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
```
### Tuning the buy tags and sell tags to display
To show only certain buy and sell tags in the displayed output, use the following two options:
```
--enter_reason_list : Comma separated list of enter signals to analyse. Default: "all"
--exit_reason_list : Comma separated list of exit signals to analyse. Default: "stop_loss,trailing_stop_loss"
--enter-reason-list : Space-separated list of enter signals to analyse. Default: "all"
--exit-reason-list : Space-separated list of exit signals to analyse. Default: "all"
```
For example:
```bash
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss"
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
```
### Outputting signal candle indicators
The real power of the buy_reasons.py script comes from the ability to print out the indicator
The real power of `freqtrade backtesting-analysis` comes from the ability to print out the indicator
values present on signal candles to allow fine-grained investigation and tuning of buy signal
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
option:
```bash
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal"
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
```
The indicators have to be present in your strategy's main DataFrame (either for your main

View File

@ -64,7 +64,10 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
### Margin mode
The possible values are: `isolated`, or `cross`(*currently unavailable*)
On top of `trading_mode` - you will also have to configure your `margin_mode`.
While freqtrade currently only supports one margin mode, this will change, and by configuring it now you're all set for future updates.
The possible values are: `isolated`, or `cross`(*currently unavailable*).
#### Isolated margin mode
@ -82,6 +85,16 @@ One account is used to share collateral between markets (trading pairs). Margin
"margin_mode": "cross"
```
## Set leverage to use
Different strategies and risk profiles will require different levels of leverage.
While you could configure one static leverage value - freqtrade offers you the flexibility to adjust this via [strategy leverage callback](strategy-callbacks.md#leverage-callback) - which allows you to use different leverages by pair, or based on some other factor benefitting your strategy result.
If not implemented, leverage defaults to 1x (no leverage).
!!! Warning
Higher leverage also equals higher risk - be sure you fully understand the implications of using leverage!
## Understand `liquidation_buffer`
*Defaults to `0.05`*

View File

@ -191,6 +191,19 @@ For example, simplified math:
!!! Tip
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
## Stoploss and Leverage
Stoploss should be thought of as "risk on this trade" - so a stoploss of 10% on a 100$ trade means you are willing to lose 10$ (10%) on this trade - which would trigger if the price moves 10% to the downside.
When using leverage, the same principle is applied - with stoploss defining the risk on the trade (the amount you are willing to lose).
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case.
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
## Changing stoploss on open trades
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).

View File

@ -841,3 +841,6 @@ class AwesomeStrategy(IStrategy):
"""
return 1.0
```
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.

View File

@ -651,6 +651,61 @@ Common arguments:
```
## Detailed backtest analysis
Advanced backtest result analysis.
More details in the [Backtesting analysis](advanced-backtesting.md#analyze-the-buyentry-and-sellexit-tags) Section.
```
usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH]
[--export-filename PATH]
[--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]]
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
optional arguments:
-h, --help show this help message and exit
--export-filename PATH, --backtest-filename PATH
Use this filename for backtest results.Requires
`--export` to be set as well. Example: `--export-filen
ame=user_data/backtest_results/backtest_today.json`
--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]
grouping output - 0: simple wins/losses by enter tag,
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
pair and enter_tag, 4: by pair, enter_ and exit_tag
(this can get quite large)
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
Comma separated list of entry signals to analyse.
Default: all. e.g. 'entry_tag_a,entry_tag_b'
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
Comma separated list of exit signals to analyse.
Default: all. e.g.
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
Comma separated list of indicators to analyse. e.g.
'close,rsi,bb_lowerband,profit_abs'
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
## List Hyperopt results
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.

View File

@ -6,6 +6,7 @@ Contains all start-commands, subcommands and CLI Interface creation.
Note: Be careful with file-scoped imports in these subfiles.
as they are parsed on startup, nothing containing optional modules should be loaded.
"""
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,

View File

@ -0,0 +1,69 @@
import logging
from pathlib import Path
from typing import Any, Dict
from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
"""
Prepare the configuration for the entry/exit reason analysis module
:param args: Cli args from Arguments()
:param method: Bot running mode
:return: Configuration
"""
config = setup_utils_configuration(args, method)
no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting',
}
if method in no_unlimited_runmodes.keys():
from freqtrade.data.btanalysis import get_latest_backtest_filename
if 'exportfilename' in config:
if config['exportfilename'].is_dir():
btfile = Path(get_latest_backtest_filename(config['exportfilename']))
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
else:
if config['exportfilename'].exists():
btfile = Path(config['exportfilename'])
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
else:
raise OperationalException(f"{config['exportfilename']} does not exist.")
else:
raise OperationalException('exportfilename not in config.')
if (not Path(signals_file).exists()):
raise OperationalException(
(f"Cannot find latest backtest signals file: {signals_file}."
"Run backtesting with `--export signals`.")
)
return config
def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
"""
Start analysis script
:param args: Cli args from Arguments()
:return: None
"""
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
# Initialize configuration
config = setup_analyze_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in analysis mode')
process_entry_exit_reasons(config['exportfilename'],
config['exchange']['pair_whitelist'],
config['analysis_groups'],
config['enter_reason_list'],
config['exit_reason_list'],
config['indicator_list']
)

View File

@ -101,6 +101,9 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
"disableparamexport", "backtest_breakdown"]
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
"exit_reason_list", "indicator_list"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-data",
"hyperopt-list", "hyperopt-show", "backtest-filter",
@ -182,8 +185,9 @@ class Arguments:
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_backtesting, start_backtesting_show,
start_convert_data, start_convert_db, start_convert_trades,
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting,
start_backtesting_show, start_convert_data,
start_convert_db, start_convert_trades,
start_create_userdir, start_download_data, start_edge,
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
start_install_ui, start_list_data, start_list_exchanges,
@ -283,6 +287,13 @@ class Arguments:
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
# Add backtesting analysis subcommand
analysis_cmd = subparsers.add_parser('backtesting-analysis',
help='Backtest Analysis module.',
parents=[_common_parser])
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])

View File

@ -614,4 +614,37 @@ AVAILABLE_CLI_OPTIONS = {
"that do not contain any parameters."),
action="store_true",
),
"analysis_groups": Arg(
"--analysis-groups",
help=("grouping output - "
"0: simple wins/losses by enter tag, "
"1: by enter_tag, "
"2: by enter_tag and exit_tag, "
"3: by pair and enter_tag, "
"4: by pair, enter_ and exit_tag (this can get quite large)"),
nargs='+',
default=['0', '1', '2'],
choices=['0', '1', '2', '3', '4'],
),
"enter_reason_list": Arg(
"--enter-reason-list",
help=("Comma separated list of entry signals to analyse. Default: all. "
"e.g. 'entry_tag_a,entry_tag_b'"),
nargs='+',
default=['all'],
),
"exit_reason_list": Arg(
"--exit-reason-list",
help=("Comma separated list of exit signals to analyse. Default: all. "
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
nargs='+',
default=['all'],
),
"indicator_list": Arg(
"--indicator-list",
help=("Comma separated list of indicators to analyse. "
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
nargs='+',
default=[],
),
}

View File

@ -95,6 +95,8 @@ class Configuration:
self._process_data_options(config)
self._process_analyze_options(config)
# Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
@ -433,6 +435,19 @@ class Configuration:
self._args_to_config(config, argname='candle_types',
logstring='Detected --candle-types: {}')
def _process_analyze_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='analysis_groups',
logstring='Analysis reason groups: {}')
self._args_to_config(config, argname='enter_reason_list',
logstring='Analysis enter tag list: {}')
self._args_to_config(config, argname='exit_reason_list',
logstring='Analysis exit tag list: {}')
self._args_to_config(config, argname='indicator_list',
logstring='Analysis indicator list: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='dry_run',

View File

@ -0,0 +1,227 @@
import logging
from pathlib import Path
from typing import List, Optional
import joblib
import pandas as pd
from tabulate import tabulate
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
load_backtest_stats)
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def _load_signal_candles(backtest_dir: Path):
if backtest_dir.is_dir():
scpf = Path(backtest_dir,
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
)
else:
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
try:
scp = open(scpf, "rb")
signal_candles = joblib.load(scp)
logger.info(f"Loaded signal candles: {str(scpf)}")
except Exception as e:
logger.error("Cannot load signal candles from pickled results: ", e)
return signal_candles
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
analysed_trades_dict = {}
analysed_trades_dict[strategy_name] = {}
try:
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
for pair in pairlist:
if pair in signal_candles[strategy_name]:
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
pair,
trades,
signal_candles[strategy_name][pair])
except Exception as e:
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
return analysed_trades_dict
def _analyze_candles_and_indicators(pair, trades, signal_candles):
buyf = signal_candles
if len(buyf) > 0:
buyf = buyf.set_index('date', drop=False)
trades_red = trades.loc[trades['pair'] == pair].copy()
trades_inds = pd.DataFrame()
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
for t, v in trades_red.open_date.items():
allinds = buyf.loc[(buyf['date'] < v)]
if allinds.shape[0] > 0:
tmp_inds = allinds.iloc[[-1]]
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0]
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag']
tmp_inds.index.rename('signal_date', inplace=True)
trades_inds = pd.concat([trades_inds, tmp_inds])
if 'signal_date' in trades_red:
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True)
trades_red.set_index('signal_date', inplace=True)
try:
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer')
except Exception as e:
raise e
return trades_red
else:
return pd.DataFrame()
def _do_group_table_output(bigdf, glist):
for g in glist:
# 0: summary wins/losses grouped by enter tag
if g == "0":
group_mask = ['enter_reason']
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \
.groupby(group_mask) \
.agg({'profit_abs': ['sum']})
wins.columns = ['profit_abs_wins']
loss = bigdf.loc[bigdf['profit_abs'] < 0] \
.groupby(group_mask) \
.agg({'profit_abs': ['sum']})
loss.columns = ['profit_abs_loss']
new = bigdf.groupby(group_mask).agg({'profit_abs': [
'count',
lambda x: sum(x > 0),
lambda x: sum(x <= 0)]})
new = pd.concat([new, wins, loss], axis=1).fillna(0)
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss'])
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
sortcols = ['total_num_buys']
_print_table(new, sortcols, show_index=True)
else:
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
'profit_ratio': ['sum', 'median', 'mean']}
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
'total_profit_pct']
sortcols = ['profit_abs_sum', 'enter_reason']
# 1: profit summaries grouped by enter_tag
if g == "1":
group_mask = ['enter_reason']
# 2: profit summaries grouped by enter_tag and exit_tag
if g == "2":
group_mask = ['enter_reason', 'exit_reason']
# 3: profit summaries grouped by pair and enter_tag
if g == "3":
group_mask = ['pair', 'enter_reason']
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
if g == "4":
group_mask = ['pair', 'enter_reason', 'exit_reason']
if group_mask:
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
new.columns = group_mask + agg_cols
new['median_profit_pct'] = new['median_profit_pct'] * 100
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
new['total_profit_pct'] = new['total_profit_pct'] * 100
_print_table(new, sortcols)
else:
logger.warning("Invalid group mask specified.")
def _print_results(analysed_trades, stratname, analysis_groups,
enter_reason_list, exit_reason_list,
indicator_list, columns=None):
if columns is None:
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
bigdf = pd.DataFrame()
for pair, trades in analysed_trades[stratname].items():
bigdf = pd.concat([bigdf, trades], ignore_index=True)
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
if analysis_groups:
_do_group_table_output(bigdf, analysis_groups)
if enter_reason_list and "all" not in enter_reason_list:
bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))]
if exit_reason_list and "all" not in exit_reason_list:
bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))]
if "all" in indicator_list:
print(bigdf)
elif indicator_list is not None:
available_inds = []
for ind in indicator_list:
if ind in bigdf:
available_inds.append(ind)
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
else:
print("\\_ No trades to show")
def _print_table(df, sortcols=None, show_index=False):
if (sortcols is not None):
data = df.sort_values(sortcols)
else:
data = df
print(
tabulate(
data,
headers='keys',
tablefmt='psql',
showindex=show_index
)
)
def process_entry_exit_reasons(backtest_dir: Path,
pairlist: List[str],
analysis_groups: Optional[List[str]] = ["0", "1", "2"],
enter_reason_list: Optional[List[str]] = ["all"],
exit_reason_list: Optional[List[str]] = ["all"],
indicator_list: Optional[List[str]] = []):
try:
backtest_stats = load_backtest_stats(backtest_dir)
for strategy_name, results in backtest_stats['strategy'].items():
trades = load_backtest_data(backtest_dir, strategy_name)
if not trades.empty:
signal_candles = _load_signal_candles(backtest_dir)
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
trades, signal_candles)
_print_results(analysed_trades_dict,
strategy_name,
analysis_groups,
enter_reason_list,
exit_reason_list,
indicator_list)
except ValueError as e:
raise OperationalException(e) from e

View File

@ -93,7 +93,7 @@ class Exchange:
:return: None
"""
self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {}

View File

@ -1297,13 +1297,14 @@ class Backtesting:
self.results['strategy_comparison'].extend(results['strategy_comparison'])
else:
self.results = results
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if self.config.get('export', 'none') in ('trades', 'signals'):
store_backtest_stats(self.config['exportfilename'], self.results)
store_backtest_stats(self.config['exportfilename'], self.results, dt_appendix)
if (self.config.get('export', 'none') == 'signals' and
self.dataprovider.runmode == RunMode.BACKTEST):
store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs)
store_backtest_signal_candles(
self.config['exportfilename'], self.processed_dfs, dt_appendix)
# Results may be mixed up now. Sort them so they follow --strategy-list order.
if 'strategy_list' in self.config and len(self.results) > 0:

View File

@ -17,21 +17,21 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
logger = logging.getLogger(__name__)
def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None:
def store_backtest_stats(
recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
"""
Stores backtest results
:param recordfilename: Path object, which can either be a filename or a directory.
Filenames will be appended with a timestamp right before the suffix
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
:param stats: Dataframe containing the backtesting statistics
:param dtappendix: Datetime to use for the filename
"""
if recordfilename.is_dir():
filename = (recordfilename /
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json')
filename = (recordfilename / f'backtest-result-{dtappendix}.json')
else:
filename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}'
).with_suffix(recordfilename.suffix)
# Store metadata separately.
@ -44,7 +44,8 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> Path:
def store_backtest_signal_candles(
recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
"""
Stores backtest trade signal candles
:param recordfilename: Path object, which can either be a filename or a directory.
@ -52,14 +53,13 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
as filename
:param stats: Dict containing the backtesting signal candles
:param dtappendix: Datetime to use for the filename
"""
if recordfilename.is_dir():
filename = (recordfilename /
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl')
filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
else:
filename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl'
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
)
file_dump_joblib(filename, candles)

View File

@ -666,8 +666,8 @@ class LocalTrade():
"""
self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() + self.realized_profit
self.close_profit = self.calc_profit_ratio(rate)
self.close_profit_abs = self.calc_profit(rate) + self.realized_profit
self.is_open = False
self.exit_order_status = 'closed'
self.open_order_id = None
@ -716,12 +716,12 @@ class LocalTrade():
"""
return len([o for o in self.orders if o.ft_order_side == self.exit_side])
def _calc_open_trade_value(self) -> float:
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
"""
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
open_trade = Decimal(amount) * Decimal(open_rate)
fees = open_trade * Decimal(self.fee_open)
if self.is_short:
return float(open_trade - fees)
@ -733,12 +733,11 @@ class LocalTrade():
Recalculate open_trade_value.
Must be called whenever open_rate, fee_open is changed.
"""
self.open_trade_value = self._calc_open_trade_value()
self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
def calculate_interest(self) -> Decimal:
"""
:param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
Calculate interest for this trade. Only applicable for Margin trading.
"""
zero = Decimal(0.0)
# If nothing was borrowed
@ -751,90 +750,77 @@ class LocalTrade():
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate)
rate = Decimal(self.interest_rate)
borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
close_trade = amount * Decimal(rate)
fees = close_trade * Decimal(fee)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
"""
Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
Calculate the Trade's close value including fees
:param rate: rate to compare with.
:return: value in stake currency of the open trade
"""
if rate is None and not self.close_rate:
return 0.0
amount = Decimal(self.amount)
amount = Decimal(amount or self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
return float(self._calc_base_close(amount, rate, self.fee_close))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate)
total_interest = self.calculate_interest()
if self.is_short:
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
return float(self._calc_base_close(amount, rate, self.fee_close))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - total_interest)
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees.
if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
else:
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional).
If fee is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
:param rate: close rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit in stake currency as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
if self.is_short:
profit = self.open_trade_value - close_trade_value
profit = open_trade_value - close_trade_value
else:
profit = close_trade_value - self.open_trade_value
profit = close_trade_value - open_trade_value
return float(f"{profit:.8f}")
def calc_profit2(self, open_rate: float, close_rate: float,
@ -845,35 +831,33 @@ class LocalTrade():
* (Decimal(1 - self.fee_close) * Decimal(close_rate)
- Decimal(1 + self.fee_open) * Decimal(open_rate)))
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
def calc_profit_ratio(
self, rate: float, amount: float = None, open_rate: float = None) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:param rate: rate to compare with.
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
:return: profit ratio as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
close_trade_value = self.calc_close_trade_value(rate, amount)
if amount is None or open_rate is None:
open_trade_value = self.open_trade_value
else:
open_trade_value = self._calc_open_trade_value(amount, open_rate)
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
long_close_zero = (not self.is_short and open_trade_value == 0.0)
leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero):
return 0.0
else:
if self.is_short:
profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
else:
profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}")

View File

@ -1,6 +1,7 @@
import asyncio
import logging
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends
@ -102,7 +103,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
min_date=min_date, max_date=max_date)
if btconfig.get('export', 'none') == 'trades':
store_backtest_stats(btconfig['exportfilename'], ApiServer._bt.results)
store_backtest_stats(
btconfig['exportfilename'], ApiServer._bt.results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
)
logger.info("Backtest finished.")

View File

@ -512,7 +512,7 @@ class RPC:
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """
currencies = []
currencies: List[Dict] = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers(cached=True)
@ -547,13 +547,12 @@ class RPC:
except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.")
continue
total = total + (est_stake or 0)
total = total + est_stake
currencies.append({
'currency': coin,
# TODO: The below can be simplified if we don't assign None to values.
'free': balance.free if balance.free is not None else 0,
'balance': balance.total if balance.total is not None else 0,
'used': balance.used if balance.used is not None else 0,
'free': balance.free,
'balance': balance.total,
'used': balance.used,
'est_stake': est_stake or 0,
'stake': stake_currency,
'side': 'long',
@ -583,7 +582,6 @@ class RPC:
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
trade_count = len(Trade.get_trades_proxy())
starting_capital_ratio = 0.0
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
@ -871,7 +869,7 @@ class RPC:
else:
errors[pair] = {
'error_msg': f"Pair {pair} is not in the current blacklist."
}
}
resp = self._rpc_blacklist()
resp['errors'] = errors
return resp

View File

@ -0,0 +1,191 @@
import logging
from unittest.mock import MagicMock, PropertyMock
import pandas as pd
import pytest
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.optimize_commands import start_backtesting
from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import get_args, patch_exchange, patched_configuration_load_config_file
@pytest.fixture(autouse=True)
def entryexitanalysis_cleanup() -> None:
yield None
Backtesting.cleanup()
def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmpdir, capsys):
caplog.set_level(logging.INFO)
default_conf.update({
"use_exit_signal": True,
"exit_profit_only": False,
"exit_profit_offset": 0.0,
"ignore_roi_if_entry_signal": False,
})
patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['ETH/BTC', 'LTC/BTC', 'ETH/BTC', 'LTC/BTC'],
'profit_ratio': [0.025, 0.05, -0.1, -0.05],
'profit_abs': [0.5, 2.0, -4.0, -2.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 08:10:00',
'2018-01-31 13:30:00', ], utc=True
),
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 09:10:00',
'2018-01-31 15:00:00', ], utc=True),
'trade_duration': [235, 40, 60, 90],
'is_open': [False, False, False, False],
'stake_amount': [0.01, 0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.10302485, 0.10302485],
'close_rate': [0.104969, 0.103541, 0.102041, 0.102541],
"is_short": [False, False, False, False],
'enter_tag': ["enter_tag_long_a",
"enter_tag_long_b",
"enter_tag_long_a",
"enter_tag_long_b"],
'exit_reason': [ExitType.ROI,
ExitType.EXIT_SIGNAL,
ExitType.STOP_LOSS,
ExitType.TRAILING_STOP_LOSS]
})
backtestmock = MagicMock(side_effect=[
{
'results': result1,
'config': default_conf,
'locks': [],
'rejected_signals': 20,
'timedout_entry_orders': 0,
'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000,
}
])
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['ETH/BTC', 'LTC/BTC', 'DASH/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--user-data-dir', str(tmpdir),
'--timeframe', '5m',
'--timerange', '1515560100-1517287800',
'--export', 'signals',
'--cache', 'none',
]
args = get_args(args)
start_backtesting(args)
captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out
assert 'EXIT REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out
base_args = [
'backtesting-analysis',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--user-data-dir', str(tmpdir),
]
# test group 0 and indicator list
args = get_args(base_args +
['--analysis-groups', "0",
'--indicator-list', "close", "rsi", "profit_abs"]
)
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert '0.5' in captured.out
assert '-4' in captured.out
assert '-2' in captured.out
assert '-3.5' in captured.out
assert '50' in captured.out
assert '0' in captured.out
assert '0.01616' in captured.out
assert '34.049' in captured.out
assert '0.104104' in captured.out
assert '47.0996' in captured.out
# test group 1
args = get_args(base_args + ['--analysis-groups', "1"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'total_profit_pct' in captured.out
assert '-3.5' in captured.out
assert '-1.75' in captured.out
assert '-7.5' in captured.out
assert '-3.75' in captured.out
assert '0' in captured.out
# test group 2
args = get_args(base_args + ['--analysis-groups', "2"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert 'total_profit_pct' in captured.out
assert '-10' in captured.out
assert '-5' in captured.out
assert '2.5' in captured.out
# test group 3
args = get_args(base_args + ['--analysis-groups', "3"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'total_profit_pct' in captured.out
assert '-7.5' in captured.out
assert '-3.75' in captured.out
assert '-1.75' in captured.out
assert '0' in captured.out
assert '2' in captured.out
# test group 4
args = get_args(base_args + ['--analysis-groups', "4"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'LTC/BTC' in captured.out
assert 'ETH/BTC' in captured.out
assert 'enter_tag_long_a' in captured.out
assert 'enter_tag_long_b' in captured.out
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
assert 'total_profit_pct' in captured.out
assert '-10' in captured.out
assert '-5' in captured.out
assert '-4' in captured.out
assert '0.5' in captured.out
assert '1' in captured.out
assert '2.5' in captured.out

View File

@ -171,7 +171,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
_backup_file(filename_last, copy_file=True)
assert not filename.is_file()
store_backtest_stats(filename, stats)
store_backtest_stats(filename, stats, '2022_01_01_15_05_13')
# get real Filename (it's btresult-<date>.json)
last_fn = get_latest_backtest_filename(filename_last.parent)
@ -194,7 +194,7 @@ def test_store_backtest_stats(testdatadir, mocker):
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
store_backtest_stats(testdatadir, {'metadata': {}})
store_backtest_stats(testdatadir, {'metadata': {}}, '2022_01_01_15_05_13')
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@ -202,7 +202,7 @@ def test_store_backtest_stats(testdatadir, mocker):
dump_mock.reset_mock()
filename = testdatadir / 'testresult.json'
store_backtest_stats(filename, {'metadata': {}})
store_backtest_stats(filename, {'metadata': {}}, '2022_01_01_15_05_13')
assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>.json
@ -216,7 +216,7 @@ def test_store_backtest_candles(testdatadir, mocker):
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# mock directory exporting
store_backtest_signal_candles(testdatadir, candle_dict)
store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13')
assert dump_mock.call_count == 1
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
@ -225,7 +225,7 @@ def test_store_backtest_candles(testdatadir, mocker):
dump_mock.reset_mock()
# mock file exporting
filename = Path(testdatadir / 'testresult')
store_backtest_signal_candles(filename, candle_dict)
store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
assert dump_mock.call_count == 1
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
# result will be testdatadir / testresult-<timestamp>_signals.pkl
@ -238,7 +238,7 @@ def test_write_read_backtest_candles(tmpdir):
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
# test directory exporting
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict)
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13')
scp = open(stored_file, "rb")
pickled_signal_candles = joblib.load(scp)
scp.close()
@ -252,7 +252,7 @@ def test_write_read_backtest_candles(tmpdir):
# test file exporting
filename = Path(tmpdir / 'testresult')
stored_file = store_backtest_signal_candles(filename, candle_dict)
stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
scp = open(stored_file, "rb")
pickled_signal_candles = joblib.load(scp)
scp.close()

View File

@ -852,8 +852,8 @@ def test_api_performance(botclient, fee):
close_rate=0.265441,
)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade)
trade = Trade(
@ -868,8 +868,8 @@ def test_api_performance(botclient, fee):
fee_open=fee.return_value,
close_rate=0.391
)
trade.close_profit = trade.calc_profit_ratio()
trade.close_profit_abs = trade.calc_profit()
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade)
Trade.commit()
@ -1384,12 +1384,14 @@ def test_api_strategies(botclient):
rc = client_get(client, f"{BASE_URI}/strategies")
assert_response(rc)
assert rc.json() == {'strategies': [
'HyperoptableStrategy',
'InformativeDecoratorTest',
'StrategyTestV2',
'StrategyTestV3',
'StrategyTestV3Futures',
'StrategyTestV3Analysis',
'StrategyTestV3Futures'
]}

View File

@ -0,0 +1,175 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
RealParameter)
class StrategyTestV3Analysis(IStrategy):
"""
Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
INTERFACE_VERSION = 3
# Minimal ROI designed for the strategy
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Optimal timeframe for the strategy
timeframe = '5m'
# Optional order type mapping
order_types = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False
}
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional time in force for orders
order_time_in_force = {
'entry': 'gtc',
'exit': 'gtc',
}
buy_params = {
'buy_rsi': 35,
# Intentionally not specified, so "default" is tested
# 'buy_plusdi': 0.4
}
sell_params = {
'sell_rsi': 74,
'sell_minusdi': 0.4
}
buy_rsi = IntParameter([0, 50], default=30, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
bot_started = False
def bot_start(self):
self.bot_started = True
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Minus Directional Indicator / Movement
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] < self.buy_rsi.value) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > self.buy_plusdi.value)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > self.buy_plusdi.value)
),
['enter_long', 'enter_tag']] = 1, 'enter_tag_long'
dataframe.loc[
(
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
),
['enter_short', 'enter_tag']] = 1, 'enter_tag_short'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > self.sell_minusdi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_long'
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
),
['exit_long', 'exit_tag']] = 1, 'exit_tag_short'
return dataframe

View File

@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list)
assert len(strategies) == 5
assert len(strategies) == 6
assert isinstance(strategies[0], dict)
@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list)
assert len(strategies) == 6
assert len(strategies) == 7
# with enum_failed=True search_all_objects() shall find 2 good strategies
# and 1 which fails to load
assert len([x for x in strategies if x['class'] is not None]) == 5
assert len([x for x in strategies if x['class'] is not None]) == 6
assert len([x for x in strategies if x['class'] is None]) == 1

View File

@ -2150,7 +2150,7 @@ def test_handle_trade(
assert trade.close_rate == 2.0 if is_short else 2.2
assert trade.close_profit == close_profit
assert trade.calc_profit() == 5.685
assert trade.calc_profit(trade.close_rate) == 5.685
assert trade.close_date is not None
assert trade.exit_reason == 'sell_signal1'

View File

@ -605,10 +605,10 @@ def test_calc_open_close_trade_price(
trade.open_rate = 2.0
trade.close_rate = 2.2
trade.recalc_open_trade_value()
assert isclose(trade._calc_open_trade_value(), open_value)
assert isclose(trade.calc_close_trade_value(), close_value)
assert isclose(trade.calc_profit(), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio()) == profit_ratio
assert isclose(trade._calc_open_trade_value(trade.amount, trade.open_rate), open_value)
assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value)
assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8))
assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
@pytest.mark.usefixtures("init_persistence")
@ -660,7 +660,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
trade.open_order_id = 'something'
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.calc_close_trade_value() == 0.0
assert trade.calc_close_trade_value(trade.close_rate) == 0.0
@pytest.mark.usefixtures("init_persistence")
@ -763,7 +763,7 @@ def test_calc_open_trade_value(
trade.update_trade(oobj) # Buy @ 2.0
# Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == result
assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == result
@pytest.mark.parametrize(
@ -813,7 +813,7 @@ def test_calc_close_trade_price(
funding_fees=funding_fees
)
trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result
@pytest.mark.parametrize(
@ -884,6 +884,17 @@ def test_calc_close_trade_price(
('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1),
('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1),
('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1),
# FUTURES, funding_fee=0
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309, futures, 0),
('binance', False, 3, 2.1, 0.0025, 2.6925, 0.13428928, futures, 0),
('binance', True, 1, 2.1, 0.0025, -3.3074999, -0.05526316, futures, 0),
('binance', True, 3, 2.1, 0.0025, -3.3074999, -0.16578947, futures, 0),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815, futures, 0),
('binance', False, 3, 1.9, 0.0025, -3.2925, -0.16421446, futures, 0),
('binance', True, 1, 1.9, 0.0025, 2.7075, 0.0452381, futures, 0),
('binance', True, 3, 1.9, 0.0025, 2.7075, 0.13571429, futures, 0),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(
@ -1128,6 +1139,11 @@ def test_calc_profit(
assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8)
assert pytest.approx(trade.calc_profit(close_rate, trade.amount,
trade.open_rate)) == round(profit, 8)
assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount,
trade.open_rate)) == round(profit_ratio, 8)
def test_migrate_new(mocker, default_conf, fee, caplog):
"""
@ -1287,7 +1303,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog)
assert trade.open_trade_value == trade._calc_open_trade_value()
assert trade.open_trade_value == trade._calc_open_trade_value(trade.amount, trade.open_rate)
assert trade.close_profit_abs is None
orders = trade.orders
@ -2299,7 +2315,7 @@ def test_recalc_trade_from_orders(fee):
)
assert fee.return_value == 0.0025
assert trade._calc_open_trade_value() == o1_trade_val
assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == o1_trade_val
assert trade.amount == o1_amount
assert trade.stake_amount == o1_cost
assert trade.open_rate == o1_rate