Merge branch 'develop' into pr/SmartManoj/6859

This commit is contained in:
Matthias
2022-06-23 20:43:35 +02:00
116 changed files with 11323 additions and 9511 deletions

View File

@@ -187,7 +187,8 @@ class Backtesting:
# since a "perfect" stoploss-exit is assumed anyway
# And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False
self.strategy.bot_start()
self.strategy.ft_bot_start()
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False):
@@ -275,8 +276,12 @@ class Backtesting:
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"])
self.futures_data[pair] = self.exchange.combine_funding_and_mark(
funding_rates=funding_rates_dict[pair],
mark_rates=mark_rates_dict[pair],
futures_funding_rate=self.config.get('futures_funding_rate', None),
)
if unavailable_pairs:
raise OperationalException(
@@ -496,7 +501,8 @@ class Backtesting:
stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)(
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
trade=trade, # type: ignore[arg-type]
current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
current_profit=current_profit, min_stake=min_stake,
max_stake=min(max_stake, stake_available))
@@ -527,15 +533,23 @@ class Backtesting:
if check_adjust_entry:
trade = self._get_adjust_trade_entry_for_candle(trade, row)
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
enter = row[SHORT_IDX] if trade.is_short else row[LONG_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
exits = self.strategy.should_exit(
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
for exit_ in exits:
t = self._get_exit_for_signal(trade, row, exit_)
if t:
return t
return None
def _get_exit_for_signal(self, trade: LocalTrade, row: Tuple,
exit_: ExitCheckTuple) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if exit_.exit_flag:
trade.close_date = exit_candle_time
exit_reason = exit_.exit_reason
@@ -562,7 +576,8 @@ class Backtesting:
if order_type == 'limit':
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
default_retval=close_rate)(
pair=trade.pair, trade=trade,
pair=trade.pair,
trade=trade, # type: ignore[arg-type]
current_time=exit_candle_time,
proposed_rate=close_rate, current_profit=current_profit,
exit_tag=exit_reason)
@@ -576,7 +591,10 @@ class Backtesting:
time_in_force = self.strategy.order_time_in_force['exit']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
pair=trade.pair,
trade=trade, # type: ignore[arg-type]
order_type='limit',
amount=trade.amount,
rate=close_rate,
time_in_force=time_in_force,
sell_reason=exit_reason, # deprecated
@@ -652,7 +670,7 @@ class Backtesting:
return self._get_exit_trade_entry_for_candle(trade, row)
def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
trade: Optional[LocalTrade], order_type: str
) -> Tuple[float, float, float, float]:
@@ -686,7 +704,7 @@ class Backtesting:
current_rate=row[OPEN_IDX],
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction,
side=direction, entry_tag=entry_tag,
) if self._can_short else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
@@ -726,8 +744,9 @@ class Backtesting:
order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None and requested_rate is None
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
pair, row, row[OPEN_IDX], stake_amount_, direction, current_time, entry_tag, trade,
order_type
)
@@ -876,28 +895,34 @@ class Backtesting:
self.protections.stop_per_pair(pair, current_time, side)
self.protections.global_stop(current_time, side)
def manage_open_orders(self, trade: LocalTrade, current_time, row: Tuple) -> bool:
def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: Tuple) -> bool:
"""
Check if any open order needs to be cancelled or replaced.
Returns True if the trade should be deleted.
"""
for order in [o for o in trade.orders if o.ft_is_open]:
if self.check_order_cancel(trade, order, current_time):
oc = self.check_order_cancel(trade, order, current_time)
if oc:
# delete trade due to order timeout
return True
elif self.check_order_replace(trade, order, current_time, row):
elif oc is None and self.check_order_replace(trade, order, current_time, row):
# delete trade due to user request
self.canceled_trade_entries += 1
return True
# default maintain trade
return False
def check_order_cancel(self, trade: LocalTrade, order: Order, current_time) -> bool:
def check_order_cancel(
self, trade: LocalTrade, order: Order, current_time: datetime) -> Optional[bool]:
"""
Check if current analyzed order has to be canceled.
Returns True if the trade should be Deleted (initial order was canceled).
Returns True if the trade should be Deleted (initial order was canceled),
False if it's Canceled
None if the order is still active.
"""
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
timedout = self.strategy.ft_check_timed_out(
trade, # type: ignore[arg-type]
order, current_time)
if timedout:
if order.side == trade.entry_side:
self.timedout_entry_orders += 1
@@ -907,12 +932,15 @@ class Backtesting:
else:
# Close additional entry order
del trade.orders[trade.orders.index(order)]
trade.open_order_id = None
return False
if order.side == trade.exit_side:
self.timedout_exit_orders += 1
# Close exit order and retry exiting on next signal.
del trade.orders[trade.orders.index(order)]
return False
trade.open_order_id = None
return False
return None
def check_order_replace(self, trade: LocalTrade, order: Order, current_time,
row: Tuple) -> bool:
@@ -926,7 +954,8 @@ class Backtesting:
if order.side == trade.entry_side and current_time > order.order_date_utc:
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
default_retval=order.price)(
trade=trade, order=order, pair=trade.pair, current_time=current_time,
trade=trade, # type: ignore[arg-type]
order=order, pair=trade.pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
entry_tag=trade.enter_tag, side=trade.trade_direction
) # default value is current order price
@@ -937,6 +966,7 @@ class Backtesting:
return False
else:
del trade.orders[trade.orders.index(order)]
trade.open_order_id = None
self.canceled_entry_orders += 1
# place new order if result was not None
@@ -1025,6 +1055,7 @@ class Backtesting:
# Close trade
open_trade_count -= 1
open_trades[pair].remove(t)
LocalTrade.trades_open.remove(t)
self.wallets.update()
# 2. Process entries.
@@ -1048,6 +1079,8 @@ class Backtesting:
open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
self.wallets.update()
for trade in list(open_trades[pair]):
# 3. Process entry orders.
@@ -1055,7 +1088,6 @@ class Backtesting:
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
# 4. Create exit orders (if any)
@@ -1065,6 +1097,7 @@ class Backtesting:
# 5. Process exit orders.
order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
trade.close_date = current_time
trade.close(order.price, show_msg=False)
@@ -1233,13 +1266,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

@@ -44,7 +44,7 @@ class EdgeCli:
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
self.strategy.bot_start()
self.strategy.ft_bot_start()
def start(self) -> None:
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])

View File

@@ -27,8 +27,7 @@ from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
@@ -62,7 +61,6 @@ class Hyperopt:
hyperopt = Hyperopt(config)
hyperopt.start()
"""
custom_hyperopt: IHyperOpt
def __init__(self, config: Dict[str, Any]) -> None:
self.buy_space: List[Dimension] = []
@@ -77,6 +75,7 @@ class Hyperopt:
self.backtesting = Backtesting(self.config)
self.pairlist = self.backtesting.pairlists.whitelist
self.custom_hyperopt: HyperOptAuto
if not self.config.get('hyperopt'):
self.custom_hyperopt = HyperOptAuto(self.config)
@@ -88,7 +87,8 @@ class Hyperopt:
self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy'])
@@ -429,7 +429,7 @@ class Hyperopt:
return new_list
i = 0
asked_non_tried: List[List[Any]] = []
is_random: List[bool] = []
is_random_non_tried: List[bool] = []
while i < 5 and len(asked_non_tried) < n_points:
if i < 3:
self.opt.cache_ = {}
@@ -438,9 +438,9 @@ class Hyperopt:
else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
is_random = [True for _ in range(len(asked))]
is_random += [rand for x, rand in zip(asked, is_random)
if x not in self.opt.Xi
and x not in asked_non_tried]
is_random_non_tried += [rand for x, rand in zip(asked, is_random)
if x not in self.opt.Xi
and x not in asked_non_tried]
asked_non_tried += [x for x in asked
if x not in self.opt.Xi
and x not in asked_non_tried]
@@ -449,7 +449,7 @@ class Hyperopt:
if asked_non_tried:
return (
asked_non_tried[:min(len(asked_non_tried), n_points)],
is_random[:min(len(asked_non_tried), n_points)]
is_random_non_tried[:min(len(asked_non_tried), n_points)]
)
else:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]

View File

@@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Union
from numpy import int64
from pandas import DataFrame, to_datetime
from tabulate import tabulate
@@ -18,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.
@@ -45,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.
@@ -53,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)
@@ -417,9 +416,9 @@ def generate_strategy_stats(pairlist: List[str],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
if not results.empty:
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
backtest_days = (max_date - min_date).days or 1
strat_stats = {
@@ -447,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@@ -501,8 +501,10 @@ def generate_strategy_stats(pairlist: List[str],
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
max_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance)
# max_relative_drawdown = Underwater
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
strat_stats.update({
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
'max_drawdown_account': max_drawdown,
@@ -781,6 +783,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),