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
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)) starting_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) buy_tag_results = generate_tag_metrics("buy_tag", 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( pairlist, stake_currency=stake_currency, starting_balance=starting_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_buy_tag': buy_tag_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, # 'days_breakdown_stats': days_breakdown_stats, '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_median': results['profit_ratio'].median() 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.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': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], 'rejected_signals': content['rejected_signals'], '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_sell_signal': config['use_sell_signal'], 'sell_profit_only': config['sell_profit_only'], 'sell_profit_offset': config['sell_profit_offset'], 'ignore_roi_if_buy_signal': config['ignore_roi_if_buy_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=starting_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, starting_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