Merge branch 'develop' into progress-bar

This commit is contained in:
Fredrik81
2020-04-08 01:56:43 +02:00
63 changed files with 1025 additions and 740 deletions

View File

@@ -6,8 +6,7 @@ This module contains the backtesting logic
import logging
from copy import deepcopy
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame
@@ -19,10 +18,8 @@ from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import file_dump_json
from freqtrade.optimize.optimize_reports import (
generate_text_table, generate_text_table_sell_reason,
generate_text_table_strategy)
from freqtrade.optimize.optimize_reports import (show_backtest_results,
store_backtest_result)
from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode
@@ -88,8 +85,8 @@ class Backtesting:
validate_config_consistency(self.config)
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
"configuration or as cli argument `--ticker-interval 5m`")
self.timeframe = str(self.config.get('ticker_interval'))
self.timeframe_min = timeframe_to_minutes(self.timeframe)
@@ -108,7 +105,7 @@ class Backtesting:
# And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False
def load_bt_data(self):
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
@@ -134,49 +131,33 @@ class Backtesting:
return data, timerange
def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if strategyname:
# Inject strategyname to filename
recordfilename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix)
logger.info(f'Dumping backtest results to {recordfilename}')
file_dump_json(recordfilename, records)
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]:
"""
Helper function to convert a processed tickerlist into a list for performance reasons.
Helper function to convert a processed dataframes into lists for performance reasons.
Used by backtest() - so keep this optimized for performance.
"""
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
ticker: Dict = {}
# Create ticker dict
data: Dict = {}
# Create dict with data
for pair, pair_data in processed.items():
pair_data.loc[:, 'buy'] = 0 # cleanup from previous run
pair_data.loc[:, 'sell'] = 0 # cleanup from previous run
ticker_data = self.strategy.advise_sell(
df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
# To avoid using data from future, we use buy/sell signals shifted
# from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed['buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True)
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
ticker[pair] = [x for x in ticker_data.itertuples()]
return ticker
data[pair] = [x for x in df_analyzed.itertuples()]
return data
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
trade_dur: int) -> float:
@@ -220,7 +201,7 @@ class Backtesting:
def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame,
partial_ticker: List, trade_count_lock: Dict,
partial_ohlcv: List, trade_count_lock: Dict,
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
trade = Trade(
@@ -235,7 +216,7 @@ class Backtesting:
)
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
# calculate win/lose forwards from buy point
for sell_row in partial_ticker:
for sell_row in partial_ohlcv:
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
@@ -259,9 +240,9 @@ class Backtesting:
close_rate=closerate,
sell_reason=sell.sell_type
)
if partial_ticker:
if partial_ohlcv:
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1]
sell_row = partial_ohlcv[-1]
bt_res = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_ratio(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open),
@@ -308,8 +289,9 @@ class Backtesting:
trades = []
trade_count_lock: Dict = {}
# Dict of ticker-lists for performance (looping lists is a lot faster than dataframes)
ticker: Dict = self._get_ticker_list(processed)
# Use dict of lists with data for performance
# (looping lists is a lot faster than pandas DataFrames)
data: Dict = self._get_ohlcv_as_lists(processed)
lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start.
@@ -319,12 +301,12 @@ class Backtesting:
# Loop timerange and get candle for each pair at that point in time
while tmp < end_date:
for i, pair in enumerate(ticker):
for i, pair in enumerate(data):
if pair not in indexes:
indexes[pair] = 0
try:
row = ticker[pair][indexes[pair]]
row = data[pair][indexes[pair]]
except IndexError:
# missing Data for one pair at the end.
# Warnings for this are shown during data loading
@@ -352,7 +334,7 @@ class Backtesting:
# since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:],
trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:],
trade_count_lock, stake_amount,
max_open_trades)
@@ -395,7 +377,7 @@ class Backtesting:
self._set_strategy(strat)
# need to reprocess data every time to populate signals
preprocessed = self.strategy.tickerdata_to_dataframe(data)
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
@@ -416,44 +398,7 @@ class Backtesting:
position_stacking=position_stacking,
)
for strategy, results in all_results.items():
if self.config.get('export', False):
self._store_backtest_result(Path(self.config['exportfilename']), results,
strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}")
table = generate_text_table(data, stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table_sell_reason(data,
stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table(data,
stake_currency=self.config['stake_currency'],
max_open_trades=self.config['max_open_trades'],
results=results.loc[results.open_at_end], skip_nan=True)
if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str):
print('=' * len(table.splitlines()[0]))
print()
if len(all_results) > 1:
# Print Strategy summary table
table = generate_text_table_strategy(self.config['stake_currency'],
self.config['max_open_trades'],
all_results=all_results)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))
print('\nFor more details, please look at the detail tables above')
if self.config.get('export', False):
store_backtest_result(self.config['exportfilename'], all_results)
# Show backtest results
show_backtest_results(self.config, data, all_results)

View File

@@ -79,8 +79,8 @@ class Hyperopt:
self.trials_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle')
self.tickerdata_pickle = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0)
self.current_best_loss = 100
@@ -134,7 +134,7 @@ class Hyperopt:
"""
Remove hyperopt pickle files to restart hyperopt.
"""
for f in [self.tickerdata_pickle, self.trials_file]:
for f in [self.data_pickle_file, self.trials_file]:
p = Path(f)
if p.is_file():
logger.info(f"Removing `{p}`.")
@@ -533,7 +533,7 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached']
processed = load(self.tickerdata_pickle)
processed = load(self.data_pickle_file)
min_date, max_date = get_timerange(processed)
@@ -660,7 +660,7 @@ class Hyperopt:
'Hyperopting with data from %s up to %s (%s days)..',
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
)
dump(preprocessed, self.tickerdata_pickle)
dump(preprocessed, self.data_pickle_file)
# We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore

View File

@@ -1,9 +1,37 @@
import logging
from datetime import timedelta
from pathlib import Path
from typing import Dict
from pandas import DataFrame
from tabulate import tabulate
from freqtrade.misc import file_dump_json
logger = logging.getLogger(__name__)
def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None:
"""
Stores backtest results to file (one file per strategy)
:param recordfilename: Destination filename
:param all_results: Dict of Dataframes, one results dataframe per strategy
"""
for strategy, results in all_results.items():
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if len(all_results) > 1:
# Inject strategy to filename
recordfilename = Path.joinpath(
recordfilename.parent,
f'{recordfilename.stem}-{strategy}').with_suffix(recordfilename.suffix)
logger.info(f'Dumping backtest results to {recordfilename}')
file_dump_json(recordfilename, records)
def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
results: DataFrame, skip_nan: bool = False) -> str:
@@ -69,12 +97,12 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_text_table_sell_reason(
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame
) -> str:
def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int,
results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
:param stake_currency: Stakecurrency used
:param max_open_trades: Max_open_trades parameter
:param results: Dataframe containing the backtest results
:return: pretty printed table with tabulate as string
"""
@@ -173,3 +201,43 @@ def generate_edge_table(results: dict) -> str:
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
all_results: Dict[str, DataFrame]):
for strategy, results in all_results.items():
print(f"Result for strategy {strategy}")
table = generate_text_table(btdata, stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table_sell_reason(stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results)
if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table(btdata,
stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results.loc[results.open_at_end], skip_nan=True)
if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str):
print('=' * len(table.splitlines()[0]))
print()
if len(all_results) > 1:
# Print Strategy summary table
table = generate_text_table_strategy(config['stake_currency'],
config['max_open_trades'],
all_results=all_results)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))
print('\nFor more details, please look at the detail tables above')