Merge branch 'develop' into backtest_fitlivepredictions
This commit is contained in:
commit
26e8a5766f
@ -100,3 +100,17 @@ freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-re
|
|||||||
The indicators have to be present in your strategy's main DataFrame (either for your main
|
The indicators have to be present in your strategy's main DataFrame (either for your main
|
||||||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||||
output.
|
output.
|
||||||
|
|
||||||
|
### Filtering the trade output by date
|
||||||
|
|
||||||
|
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:
|
||||||
|
|
||||||
|
```
|
||||||
|
--timerange : Timerange to filter output trades, start date inclusive, end date exclusive. e.g. 20220101-20221231
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||||
|
```
|
||||||
|
@ -83,7 +83,7 @@ from pathlib import Path
|
|||||||
project_root = "somedir/freqtrade"
|
project_root = "somedir/freqtrade"
|
||||||
i=0
|
i=0
|
||||||
try:
|
try:
|
||||||
os.chdirdir(project_root)
|
os.chdir(project_root)
|
||||||
assert Path('LICENSE').is_file()
|
assert Path('LICENSE').is_file()
|
||||||
except:
|
except:
|
||||||
while i<4 and (not Path('LICENSE').is_file()):
|
while i<4 and (not Path('LICENSE').is_file()):
|
||||||
|
@ -722,6 +722,7 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
|||||||
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
||||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||||
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||||
|
[--timerange YYYYMMDD-[YYYYMMDD]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -744,6 +745,10 @@ optional arguments:
|
|||||||
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||||
Comma separated list of indicators to analyse. e.g.
|
Comma separated list of indicators to analyse. e.g.
|
||||||
'close,rsi,bb_lowerband,profit_abs'
|
'close,rsi,bb_lowerband,profit_abs'
|
||||||
|
--timerange YYYYMMDD-[YYYYMMDD]
|
||||||
|
Timerange to filter trades for analysis,
|
||||||
|
start inclusive, end exclusive. e.g.
|
||||||
|
20220101-20220201
|
||||||
|
|
||||||
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).
|
||||||
|
@ -60,10 +60,4 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
logger.info('Starting freqtrade in analysis mode')
|
logger.info('Starting freqtrade in analysis mode')
|
||||||
|
|
||||||
process_entry_exit_reasons(config['exportfilename'],
|
process_entry_exit_reasons(config)
|
||||||
config['exchange']['pair_whitelist'],
|
|
||||||
config['analysis_groups'],
|
|
||||||
config['enter_reason_list'],
|
|
||||||
config['exit_reason_list'],
|
|
||||||
config['indicator_list']
|
|
||||||
)
|
|
||||||
|
@ -106,7 +106,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
"disableparamexport", "backtest_breakdown"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||||
"exit_reason_list", "indicator_list"]
|
"exit_reason_list", "indicator_list", "timerange"]
|
||||||
|
|
||||||
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-freqaimodels",
|
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||||
|
@ -462,6 +462,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='indicator_list',
|
self._args_to_config(config, argname='indicator_list',
|
||||||
logstring='Analysis indicator list: {}')
|
logstring='Analysis indicator list: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='timerange',
|
||||||
|
logstring='Filter trades by timerange: {}')
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='dry_run',
|
self._args_to_config(config, argname='dry_run',
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
import joblib
|
import joblib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
|
from freqtrade.constants import Config
|
||||||
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
||||||
load_backtest_stats)
|
load_backtest_stats)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -152,37 +153,55 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
logger.warning("Invalid group mask specified.")
|
logger.warning("Invalid group mask specified.")
|
||||||
|
|
||||||
|
|
||||||
def _print_results(analysed_trades, stratname, analysis_groups,
|
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
||||||
enter_reason_list, exit_reason_list,
|
if timerange:
|
||||||
indicator_list, columns=None):
|
if timerange.starttype == 'date':
|
||||||
if columns is None:
|
df = df.loc[(df[df_date_col] >= timerange.startdt)]
|
||||||
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
|
if timerange.stoptype == 'date':
|
||||||
|
df = df.loc[(df[df_date_col] < timerange.stopdt)]
|
||||||
|
return df
|
||||||
|
|
||||||
bigdf = pd.DataFrame()
|
|
||||||
|
def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
||||||
|
if enter_reason_list and "all" not in enter_reason_list:
|
||||||
|
df = df.loc[(df['enter_reason'].isin(enter_reason_list))]
|
||||||
|
|
||||||
|
if exit_reason_list and "all" not in exit_reason_list:
|
||||||
|
df = df.loc[(df['exit_reason'].isin(exit_reason_list))]
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_results(analysed_trades, stratname,
|
||||||
|
enter_reason_list, exit_reason_list,
|
||||||
|
timerange=None):
|
||||||
|
res_df = pd.DataFrame()
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
bigdf = pd.concat([bigdf, trades], ignore_index=True)
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
|
res_df = _select_rows_within_dates(res_df, timerange)
|
||||||
|
|
||||||
|
if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns):
|
||||||
|
res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list)
|
||||||
|
|
||||||
|
return res_df
|
||||||
|
|
||||||
|
|
||||||
|
def print_results(res_df, analysis_groups, indicator_list):
|
||||||
|
if res_df.shape[0] > 0:
|
||||||
if analysis_groups:
|
if analysis_groups:
|
||||||
_do_group_table_output(bigdf, analysis_groups)
|
_do_group_table_output(res_df, 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:
|
if "all" in indicator_list:
|
||||||
print(bigdf)
|
print(res_df)
|
||||||
elif indicator_list is not None:
|
elif indicator_list is not None:
|
||||||
available_inds = []
|
available_inds = []
|
||||||
for ind in indicator_list:
|
for ind in indicator_list:
|
||||||
if ind in bigdf:
|
if ind in res_df:
|
||||||
available_inds.append(ind)
|
available_inds.append(ind)
|
||||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||||
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
|
_print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False)
|
||||||
else:
|
else:
|
||||||
print("\\_ No trades to show")
|
print("\\No trades to show")
|
||||||
|
|
||||||
|
|
||||||
def _print_table(df, sortcols=None, show_index=False):
|
def _print_table(df, sortcols=None, show_index=False):
|
||||||
@ -201,27 +220,34 @@ def _print_table(df, sortcols=None, show_index=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def process_entry_exit_reasons(backtest_dir: Path,
|
def process_entry_exit_reasons(config: Config):
|
||||||
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:
|
try:
|
||||||
backtest_stats = load_backtest_stats(backtest_dir)
|
analysis_groups = config.get('analysis_groups', [])
|
||||||
|
enter_reason_list = config.get('enter_reason_list', ["all"])
|
||||||
|
exit_reason_list = config.get('exit_reason_list', ["all"])
|
||||||
|
indicator_list = config.get('indicator_list', [])
|
||||||
|
|
||||||
|
timerange = TimeRange.parse_timerange(None if config.get(
|
||||||
|
'timerange') is None else str(config.get('timerange')))
|
||||||
|
|
||||||
|
backtest_stats = load_backtest_stats(config['exportfilename'])
|
||||||
|
|
||||||
for strategy_name, results in backtest_stats['strategy'].items():
|
for strategy_name, results in backtest_stats['strategy'].items():
|
||||||
trades = load_backtest_data(backtest_dir, strategy_name)
|
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
||||||
|
|
||||||
if not trades.empty:
|
if not trades.empty:
|
||||||
signal_candles = _load_signal_candles(backtest_dir)
|
signal_candles = _load_signal_candles(config['exportfilename'])
|
||||||
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
|
analysed_trades_dict = _process_candles_and_indicators(
|
||||||
trades, signal_candles)
|
config['exchange']['pair_whitelist'], strategy_name,
|
||||||
_print_results(analysed_trades_dict,
|
trades, signal_candles)
|
||||||
strategy_name,
|
|
||||||
analysis_groups,
|
res_df = prepare_results(analysed_trades_dict, strategy_name,
|
||||||
enter_reason_list,
|
enter_reason_list, exit_reason_list,
|
||||||
exit_reason_list,
|
timerange=timerange)
|
||||||
indicator_list)
|
|
||||||
|
print_results(res_df,
|
||||||
|
analysis_groups,
|
||||||
|
indicator_list)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
@ -740,6 +740,24 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
|
def _force_entry_validations(self, pair: str, order_side: SignalDirection):
|
||||||
|
if not self._freqtrade.config.get('force_entry_enable', False):
|
||||||
|
raise RPCException('Force_entry not enabled.')
|
||||||
|
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
|
if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
|
||||||
|
raise RPCException("Can't go short on Spot markets.")
|
||||||
|
|
||||||
|
if pair not in self._freqtrade.exchange.get_markets(tradable_only=True):
|
||||||
|
raise RPCException('Symbol does not exist or market is not active.')
|
||||||
|
# Check if pair quote currency equals to the stake currency.
|
||||||
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
|
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
|
||||||
|
raise RPCException(
|
||||||
|
f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
|
||||||
|
|
||||||
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
|
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
|
||||||
order_type: Optional[str] = None,
|
order_type: Optional[str] = None,
|
||||||
order_side: SignalDirection = SignalDirection.LONG,
|
order_side: SignalDirection = SignalDirection.LONG,
|
||||||
@ -750,21 +768,8 @@ class RPC:
|
|||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
"""
|
"""
|
||||||
|
self._force_entry_validations(pair, order_side)
|
||||||
|
|
||||||
if not self._freqtrade.config.get('force_entry_enable', False):
|
|
||||||
raise RPCException('Force_entry not enabled.')
|
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
|
||||||
raise RPCException('trader is not running')
|
|
||||||
|
|
||||||
if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
|
|
||||||
raise RPCException("Can't go short on Spot markets.")
|
|
||||||
|
|
||||||
# Check if pair quote currency equals to the stake currency.
|
|
||||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
|
||||||
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
|
|
||||||
raise RPCException(
|
|
||||||
f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.')
|
|
||||||
# check if valid pair
|
# check if valid pair
|
||||||
|
|
||||||
# check if pair already has an open pair
|
# check if pair already has an open pair
|
||||||
|
@ -189,3 +189,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
|
|||||||
assert '0.5' in captured.out
|
assert '0.5' in captured.out
|
||||||
assert '1' in captured.out
|
assert '1' in captured.out
|
||||||
assert '2.5' in captured.out
|
assert '2.5' in captured.out
|
||||||
|
|
||||||
|
# test date filtering
|
||||||
|
args = get_args(base_args + ['--timerange', "20180129-20180130"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' not in captured.out
|
||||||
|
@ -1056,6 +1056,10 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
|||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
assert trade.open_rate == 0.0001
|
||||||
|
|
||||||
|
with pytest.raises(RPCException,
|
||||||
|
match=r'Symbol does not exist or market is not active.'):
|
||||||
|
rpc._rpc_force_entry('LTC/NOTHING', 0.0001)
|
||||||
|
|
||||||
# Test buy pair not with stakes
|
# Test buy pair not with stakes
|
||||||
with pytest.raises(RPCException,
|
with pytest.raises(RPCException,
|
||||||
match=r'Wrong pair selected. Only pairs with stake-currency.*'):
|
match=r'Wrong pair selected. Only pairs with stake-currency.*'):
|
||||||
|
Loading…
Reference in New Issue
Block a user