Merge branch 'develop' into bot-start

This commit is contained in:
Sam Germain
2022-04-29 22:23:42 -06:00
25 changed files with 187 additions and 116 deletions

View File

@@ -0,0 +1,40 @@
import hashlib
from copy import deepcopy
from pathlib import Path
from typing import Union
import rapidjson
def get_strategy_run_id(strategy) -> str:
"""
Generate unique identification hash for a backtest run. Identical config and strategy file will
always return an identical hash.
:param strategy: strategy object.
:return: hex string id.
"""
digest = hashlib.sha1()
config = deepcopy(strategy.config)
# Options that have no impact on results of individual backtest.
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
for k in not_important_keys:
if k in config:
del config[k]
# Explicitly allow NaN values (e.g. max_open_trades).
# as it does not matter for getting the hash.
digest.update(rapidjson.dumps(config, default=str,
number_mode=rapidjson.NM_NAN).encode('utf-8'))
# Include _ft_params_from_file - so changing parameter files cause cache eviction
digest.update(rapidjson.dumps(
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8'))
with open(strategy.__file__, 'rb') as fp:
digest.update(fp.read())
return digest.hexdigest().lower()
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
"""Return metadata filename for specified backtest results file."""
filename = Path(filename)
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')

View File

@@ -24,8 +24,8 @@ from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType
TradingMode)
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import get_strategy_run_id
from freqtrade.mixins import LoggingMixin
from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.bt_progress import BTProgress
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
store_backtest_signal_candles,
@@ -54,6 +54,11 @@ ESHORT_IDX = 8 # Exit short
ENTER_TAG_IDX = 9
EXIT_TAG_IDX = 10
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
HEADERS = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
class Backtesting:
"""
@@ -265,10 +270,18 @@ class Backtesting:
candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"])
)
# Combine data to avoid combining the data per trade.
unavailable_pairs = []
for pair in self.pairlists.whitelist:
if pair not in self.exchange._leverage_tiers:
unavailable_pairs.append(pair)
continue
self.futures_data[pair] = funding_rates_dict[pair].merge(
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
if unavailable_pairs:
raise OperationalException(
f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. "
"It is therefore impossible to backtest with this pair at the moment.")
else:
self.futures_data = {}
@@ -306,10 +319,7 @@ class Backtesting:
:param processed: a processed dictionary with format {pair, data}, which gets cleared to
optimize memory usage!
"""
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed))
@@ -321,7 +331,7 @@ class Backtesting:
if not pair_data.empty:
# Cleanup from prior runs
pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore')
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
df_analyzed = self.strategy.advise_exit(
self.strategy.advise_entry(pair_data, {'pair': pair}),
@@ -340,7 +350,7 @@ class Backtesting:
# To avoid using data from future, we use entry/exit signals shifted
# from the previous candle
for col in headers[5:]:
for col in HEADERS[5:]:
tag_col = col in ('enter_tag', 'exit_tag')
if col in df_analyzed.columns:
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].replace(
@@ -352,7 +362,7 @@ class Backtesting:
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
data[pair] = df_analyzed[HEADERS].values.tolist() if not df_analyzed.empty else []
return data
def _get_close_rate(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
@@ -516,10 +526,10 @@ class Backtesting:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exit_ = self.strategy.should_exit(
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
enter=enter, exit_=exit_,
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
@@ -541,7 +551,8 @@ class Backtesting:
default_retval=closerate)(
pair=trade.pair, trade=trade,
current_time=exit_candle_time,
proposed_rate=closerate, current_profit=current_profit)
proposed_rate=closerate, current_profit=current_profit,
exit_tag=exit_.exit_reason)
# We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately
if trade.is_short:
@@ -568,6 +579,7 @@ class Backtesting:
len(row) > EXIT_TAG_IDX
and row[EXIT_TAG_IDX] is not None
and len(row[EXIT_TAG_IDX]) > 0
and exit_.exit_type in (ExitType.EXIT_SIGNAL,)
):
trade.exit_reason = row[EXIT_TAG_IDX]
@@ -626,9 +638,7 @@ class Backtesting:
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
for det_row in detail_data[headers].values.tolist():
for det_row in detail_data[HEADERS].values.tolist():
res = self._get_exit_trade_entry_for_candle(trade, det_row)
if res:
return res

View File

@@ -468,6 +468,7 @@ class Hyperopt:
self.backtesting.exchange._api = None
self.backtesting.exchange._api_async = None
self.backtesting.exchange.loop = None # type: ignore
self.backtesting.exchange._loop_lock = None # type: ignore
# self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore

View File

@@ -11,8 +11,8 @@ from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change,
calculate_max_drawdown)
from freqtrade.misc import (decimals_per_coin, file_dump_joblib, file_dump_json,
get_backtest_metadata_filename, round_coin_value)
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
logger = logging.getLogger(__name__)