def test_calculate_max_drawdown2(): values = [ 0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024, -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872, -0.025782, 0.010400, 0.012374, 0.012467, 0.114741, 0.010303, 0.010088, -0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693, 0.010711 ] dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))] df = DataFrame(zip(values, dates), columns=['profit', 'open_date']) # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) df1 = df.copy() drawdown, hdate, ldate, hval, lval = calculate_max_drawdown( df, date_col='open_date', value_col='profit') # Ensure df has not been altered. assert df.equals(df1) assert isinstance(drawdown, float) # High must be before low assert hdate < ldate # High value must be higher than low value assert hval > lval assert drawdown == 0.091755 df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'): calculate_max_drawdown(df, date_col='open_date', value_col='profit')
def generate_strategy_comparison(all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies :return: List of Dicts containing the metrics per Strategy """ tabular_data = [] for strategy, results in all_results.items(): tabular_data.append( _generate_result_line(results['results'], results['config']['dry_run_wallet'], strategy)) try: max_drawdown_per, _, _, _, _ = calculate_max_drawdown( results['results'], value_col='profit_ratio') max_drawdown_abs, _, _, _, _ = calculate_max_drawdown( results['results'], value_col='profit_abs') except ValueError: max_drawdown_per = 0 max_drawdown_abs = 0 tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2) tabular_data[-1]['max_drawdown_abs'] = \ round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False) return tabular_data
def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) drawdown, h, low = calculate_max_drawdown(bt_data) assert isinstance(drawdown, float) assert pytest.approx(drawdown) == 0.21142322 assert isinstance(h, Timestamp) assert isinstance(low, Timestamp) assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') with pytest.raises(ValueError, match='Trade dataframe empty.'): drawdown, h, low = calculate_max_drawdown(DataFrame())
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, timeframe: str) -> make_subplots: """ Add scatter points indicating max drawdown """ try: max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades) drawdown = go.Scatter( x=[highdate, lowdate], y=[ df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'], df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'], ], mode='markers', name=f"Max drawdown {max_drawdown:.2%}", text=f"Max drawdown {max_drawdown:.2%}", marker=dict(symbol='square-open', size=9, line=dict(width=2), color='green')) fig.add_trace(drawdown, row, 1) except ValueError: logger.warning("No trades found - not plotting max drawdown.") return fig
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ Evaluate recent trades for drawdown ... """ look_back_until = date_now - timedelta(minutes=self._lookback_period) trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) trades_df = pd.DataFrame([trade.to_json() for trade in trades]) if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None # Drawdown is always positive try: drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') except ValueError: return False, None, None if drawdown > self._max_allowed_drawdown: self.log_once( f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}" f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) return False, None, None
def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, config: Dict, processed: Dict[str, DataFrame], backtest_stats: Dict[str, Any], *args, **kwargs) -> float: """ Objective function, returns smaller number for more optimal results. Uses Calmar Ratio calculation. """ total_profit = backtest_stats["profit_total"] days_period = (max_date - min_date).days # adding slippage of 0.1% per trade total_profit = total_profit - 0.0005 expected_returns_mean = total_profit.sum() / days_period * 100 # calculate max drawdown try: _, _, _, high_val, low_val = calculate_max_drawdown( results, value_col="profit_abs") max_drawdown = (high_val - low_val) / high_val except ValueError: max_drawdown = 0 if max_drawdown != 0: calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365) else: # Define high (negative) calmar ratio to be clear that this is NOT optimal. calmar_ratio = -20.0 # print(expected_returns_mean, max_drawdown, calmar_ratio) return -calmar_ratio
def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float: total_profit = results["profit_abs"].sum() try: max_drawdown_abs = calculate_max_drawdown( results, value_col="profit_abs")[5] except ValueError: max_drawdown_abs = 0 return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT))
def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest-result_new.json" bt_data = load_backtest_data(filename) _, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown( bt_data, value_col="profit_abs") assert isinstance(drawdown, float) assert pytest.approx(drawdown) == 0.12071099 assert isinstance(hdate, Timestamp) assert isinstance(lowdate, Timestamp) assert isinstance(hval, float) assert isinstance(lval, float) assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC') assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC') underwater = calculate_underwater(bt_data) assert isinstance(underwater, DataFrame) with pytest.raises(ValueError, match='Trade dataframe empty.'): calculate_max_drawdown(DataFrame()) with pytest.raises(ValueError, match='Trade dataframe empty.'): calculate_underwater(DataFrame())
def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, *args, **kwargs) -> float: """ Objective function. Uses profit ratio weighted max_drawdown when drawdown is available. Otherwise directly optimizes profit ratio. """ total_profit = results['profit_abs'].sum() try: max_drawdown = calculate_max_drawdown(results, value_col='profit_abs') except ValueError: # No losing trade, therefore no drawdown. return -total_profit return -total_profit / max_drawdown[0]
def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, *args, **kwargs) -> float: """ Objective function, returns smaller number for better results. """ profit_threshold = 0 if IGNORE_SMALL_PROFITS: profit_threshold = SMALL_PROFITS_THRESHOLD total_profit = results['profit_ratio'].sum() total_win = len(results[(results['profit_ratio'] > profit_threshold)]) total_lose = len(results[(results['profit_ratio'] <= 0)]) average_profit = results['profit_ratio'].mean() * 100 sortino_ratio = sortino_daily(results, trade_count, min_date, max_date) trade_duration = results['trade_duration'].mean() max_drawdown = 100 try: max_drawdown, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') except: pass if total_lose == 0: total_lose = 1 profit_loss = (1 - total_profit / EXPECTED_MAX_PROFIT) * TOTAL_PROFIT_WEIGHT win_lose_loss = (1 - (total_win / total_lose)) * WIN_LOSS_WEIGHT average_profit_loss = 1 - (min(average_profit, AVERAGE_PROFIT_THRESHOLD) * AVERAGE_PROFIT_WEIGHT) sortino_ratio_loss = SORTINO_WEIGHT * sortino_ratio drawdown_loss = max_drawdown * DRAWDOWN_WEIGHT duration_loss = DURATION_WEIGHT * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = profit_loss + win_lose_loss + average_profit_loss + sortino_ratio_loss + drawdown_loss + duration_loss return result
def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow ) -> Dict[str, Any]: """ :param btdata: Backtest data :param all_results: backtest result - dictionary in the form: { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): continue config = content['config'] max_open_trades = config['max_open_trades'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) daily_stats = generate_daily_stats(results) results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_percent'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'market_change': market_change, 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], # Parameters relevant for backtesting 'stoploss': config['stoploss'], 'trailing_stop': config.get('trailing_stop', False), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), 'minimal_roi': config['minimal_roi'], 'use_sell_signal': config['ask_strategy']['use_sell_signal'], 'sell_profit_only': config['ask_strategy']['sell_profit_only'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, } result['strategy'][strategy] = strat_stats try: max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( results, value_col='profit_percent') strat_stats.update({ 'max_drawdown': max_drawdown, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, }) except ValueError: strat_stats.update({ 'max_drawdown': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end_ts': 0, }) strategy_results = generate_strategy_metrics(all_results=all_results) result['strategy_comparison'] = strategy_results return result
def generate_strategy_stats(pairlist: List[str], strategy: str, content: Dict[str, Any], min_date: datetime, max_date: datetime, market_change: float) -> Dict[str, Any]: """ :param pairlist: List of pairs to backtest :param strategy: Strategy name :param content: Backtest result data in the format: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date :param market_change: float indicating the market change :return: Dictionary containing results per strategy and a strategy summary. """ results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): return {} config = content['config'] max_open_trades = min(config['max_open_trades'], len(pairlist)) start_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency, starting_balance=start_balance, results=results, skip_nan=False) enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance, results=results, skip_nan=False) exit_reason_stats = generate_exit_reason_stats( max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics( pairlist, stake_currency=stake_currency, starting_balance=start_balance, results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) trade_stats = generate_trading_stats(results) best_pair = max( [pair for pair in pair_results if pair['key'] != 'TOTAL'], 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 backtest_days = (max_date - min_date).days or 1 strat_stats = { 'trades': results.to_dict(orient='records'), 'locks': [lock.to_json() for lock in content['locks']], 'best_pair': best_pair, 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'results_per_enter_tag': enter_tag_results, 'exit_reason_summary': exit_reason_stats, 'left_open_trades': left_open_results, # 'days_breakdown_stats': days_breakdown_stats, 'total_trades': len(results), 'trade_count_long': len(results.loc[~results['is_short']]), 'trade_count_short': len(results.loc[results['is_short']]), 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / start_balance, 'profit_total_long': results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance, 'profit_total_short': results.loc[results['is_short'], 'profit_abs'].sum() / start_balance, 'profit_total_abs': results['profit_abs'].sum(), 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_start_ts': int(min_date.timestamp() * 1000), 'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_end_ts': int(max_date.timestamp() * 1000), 'backtest_days': backtest_days, 'backtest_run_start_ts': content['backtest_start_time'], 'backtest_run_end_ts': content['backtest_end_time'], 'trades_per_day': round(len(results) / backtest_days, 2), 'market_change': market_change, 'pairlist': pairlist, 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'starting_balance': start_balance, 'dry_run_wallet': start_balance, 'final_balance': content['final_balance'], 'rejected_signals': content['rejected_signals'], 'timedout_entry_orders': content['timedout_entry_orders'], 'timedout_exit_orders': content['timedout_exit_orders'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], 'timeframe_detail': config.get('timeframe_detail', ''), 'timerange': config.get('timerange', ''), 'enable_protections': config.get('enable_protections', False), 'strategy_name': strategy, # Parameters relevant for backtesting 'stoploss': config['stoploss'], 'trailing_stop': config.get('trailing_stop', False), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), 'use_custom_stoploss': config.get('use_custom_stoploss', False), 'minimal_roi': config['minimal_roi'], 'use_exit_signal': config['use_exit_signal'], 'exit_profit_only': config['exit_profit_only'], 'exit_profit_offset': config['exit_profit_offset'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'], **daily_stats, **trade_stats } try: max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = calculate_max_drawdown(results, value_col='profit_abs', starting_balance=start_balance) strat_stats.update({ 'max_drawdown': max_drawdown_legacy, # Deprecated - do not use 'max_drawdown_account': max_drawdown, 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT), 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT), 'drawdown_end_ts': drawdown_end.timestamp() * 1000, 'max_drawdown_low': low_val, 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results, start_balance) strat_stats.update({'csum_min': csum_min, 'csum_max': csum_max}) except ValueError: strat_stats.update({ 'max_drawdown': 0.0, 'max_drawdown_account': 0.0, 'max_drawdown_abs': 0.0, 'max_drawdown_low': 0.0, 'max_drawdown_high': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end_ts': 0, 'csum_min': 0, 'csum_max': 0 }) return strat_stats
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame], min_date: Arrow, max_date: Arrow) -> Dict[str, Any]: """ :param config: Configuration object used for backtest :param btdata: Backtest data :param all_results: backtest result - dictionary with { Strategy: results}. :param min_date: Backtest start date :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ stake_currency = config['stake_currency'] max_open_trades = config['max_open_trades'] result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, results in all_results.items(): pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats( max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics( btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) daily_stats = generate_daily_stats(results) results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_percent'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.timestamp * 1000, 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.timestamp * 1000, 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'market_change': market_change, 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], **daily_stats, } result['strategy'][strategy] = strat_stats try: max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( results, value_col='profit_percent') strat_stats.update({ 'max_drawdown': max_drawdown, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, }) except ValueError: strat_stats.update({ 'max_drawdown': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end_ts': 0, }) strategy_results = generate_strategy_metrics( stake_currency=stake_currency, max_open_trades=max_open_trades, all_results=all_results) result['strategy_comparison'] = strategy_results return result
def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow) -> Dict[str, Any]: """ :param btdata: Backtest data :param all_results: backtest result - dictionary in the form: { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): continue config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) starting_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats( max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics( btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) best_pair = max( [pair for pair in pair_results if pair['key'] != 'TOTAL'], 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 results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), 'locks': [lock.to_json() for lock in content['locks']], 'best_pair': best_pair, 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_days': backtest_days, 'backtest_run_start_ts': content['backtest_start_time'], 'backtest_run_end_ts': content['backtest_end_time'], 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'market_change': market_change, 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], 'timerange': config.get('timerange', ''), 'enable_protections': config.get('enable_protections', False), 'strategy_name': strategy, # Parameters relevant for backtesting 'stoploss': config['stoploss'], 'trailing_stop': config.get('trailing_stop', False), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), 'use_custom_stoploss': config.get('use_custom_stoploss', False), 'minimal_roi': config['minimal_roi'], 'use_sell_signal': config['ask_strategy']['use_sell_signal'], 'sell_profit_only': config['ask_strategy']['sell_profit_only'], 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, } result['strategy'][strategy] = strat_stats try: max_drawdown, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( results, value_col='profit_abs') strat_stats.update({ 'max_drawdown': max_drawdown, 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, 'max_drawdown_low': low_val, 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results, starting_balance) strat_stats.update({'csum_min': csum_min, 'csum_max': csum_max}) except ValueError: strat_stats.update({ 'max_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'max_drawdown_low': 0.0, 'max_drawdown_high': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end_ts': 0, 'csum_min': 0, 'csum_max': 0 }) strategy_results = generate_strategy_metrics(all_results=all_results) result['strategy_comparison'] = strategy_results return result