• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.48
/freqtrade/optimize/optimize_reports/optimize_reports.py
1
import logging
1✔
2
from copy import deepcopy
1✔
3
from datetime import datetime, timedelta, timezone
1✔
4
from typing import Any, Dict, List, Tuple, Union
1✔
5

6
import numpy as np
1✔
7
from pandas import DataFrame, Series, concat, to_datetime
1✔
8

9
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT
1✔
10
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
1✔
11
                                    calculate_expectancy, calculate_market_change,
12
                                    calculate_max_drawdown, calculate_sharpe, calculate_sortino)
13
from freqtrade.types import BacktestResultType
1✔
14
from freqtrade.util import decimals_per_coin, fmt_coin
1✔
15

16

17
logger = logging.getLogger(__name__)
1✔
18

19

20
def generate_trade_signal_candles(preprocessed_df: Dict[str, DataFrame],
1✔
21
                                  bt_results: Dict[str, Any]) -> DataFrame:
22
    signal_candles_only = {}
1✔
23
    for pair in preprocessed_df.keys():
1✔
24
        signal_candles_only_df = DataFrame()
1✔
25

26
        pairdf = preprocessed_df[pair]
1✔
27
        resdf = bt_results['results']
1✔
28
        pairresults = resdf.loc[(resdf["pair"] == pair)]
1✔
29

30
        if pairdf.shape[0] > 0:
1✔
31
            for t, v in pairresults.open_date.items():
1✔
32
                allinds = pairdf.loc[(pairdf['date'] < v)]
1✔
33
                signal_inds = allinds.iloc[[-1]]
1✔
34
                signal_candles_only_df = concat([
1✔
35
                    signal_candles_only_df.infer_objects(),
36
                    signal_inds.infer_objects()])
37

38
            signal_candles_only[pair] = signal_candles_only_df
1✔
39
    return signal_candles_only
1✔
40

41

42
def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame],
1✔
43
                              rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
44
    rejected_candles_only = {}
1✔
45
    for pair, signals in rejected_dict.items():
1✔
46
        rejected_signals_only_df = DataFrame()
×
47
        pairdf = preprocessed_df[pair]
×
48

49
        for t in signals:
×
50
            data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy()
×
51
            data_df_row['pair'] = pair
×
52
            data_df_row['enter_tag'] = t[1]
×
53

54
            rejected_signals_only_df = concat([
×
55
                rejected_signals_only_df.infer_objects(),
56
                data_df_row.infer_objects()])
57

58
        rejected_candles_only[pair] = rejected_signals_only_df
×
59
    return rejected_candles_only
1✔
60

61

62
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
1✔
63
    """
64
    Generate one result dict, with "first_column" as key.
65
    """
66
    profit_sum = result['profit_ratio'].sum()
1✔
67
    # (end-capital - starting capital) / starting capital
68
    profit_total = result['profit_abs'].sum() / starting_balance
1✔
69

70
    return {
1✔
71
        'key': first_column,
72
        'trades': len(result),
73
        'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
74
        'profit_mean_pct': round(result['profit_ratio'].mean() * 100.0, 2
75
                                 ) if len(result) > 0 else 0.0,
76
        'profit_sum': profit_sum,
77
        'profit_sum_pct': round(profit_sum * 100.0, 2),
78
        'profit_total_abs': result['profit_abs'].sum(),
79
        'profit_total': profit_total,
80
        'profit_total_pct': round(profit_total * 100.0, 2),
81
        'duration_avg': str(timedelta(
82
                            minutes=round(result['trade_duration'].mean()))
83
                            ) if not result.empty else '0:00',
84
        # 'duration_max': str(timedelta(
85
        #                     minutes=round(result['trade_duration'].max()))
86
        #                     ) if not result.empty else '0:00',
87
        # 'duration_min': str(timedelta(
88
        #                     minutes=round(result['trade_duration'].min()))
89
        #                     ) if not result.empty else '0:00',
90
        'wins': len(result[result['profit_abs'] > 0]),
91
        'draws': len(result[result['profit_abs'] == 0]),
92
        'losses': len(result[result['profit_abs'] < 0]),
93
        'winrate': len(result[result['profit_abs'] > 0]) / len(result) if len(result) else 0.0,
94
    }
95

96

97
def generate_pair_metrics(pairlist: List[str], stake_currency: str, starting_balance: int,
1✔
98
                          results: DataFrame, skip_nan: bool = False) -> List[Dict]:
99
    """
100
    Generates and returns a list  for the given backtest data and the results dataframe
101
    :param pairlist: Pairlist used
102
    :param stake_currency: stake-currency - used to correctly name headers
103
    :param starting_balance: Starting balance
104
    :param results: Dataframe containing the backtest results
105
    :param skip_nan: Print "left open" open trades
106
    :return: List of Dicts containing the metrics per pair
107
    """
108

109
    tabular_data = []
1✔
110

111
    for pair in pairlist:
1✔
112
        result = results[results['pair'] == pair]
1✔
113
        if skip_nan and result['profit_abs'].isnull().all():
1✔
114
            continue
1✔
115

116
        tabular_data.append(_generate_result_line(result, starting_balance, pair))
1✔
117

118
    # Sort by total profit %:
119
    tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
1✔
120

121
    # Append Total
122
    tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
1✔
123
    return tabular_data
1✔
124

125

126
def generate_tag_metrics(tag_type: str,
1✔
127
                         starting_balance: int,
128
                         results: DataFrame,
129
                         skip_nan: bool = False) -> List[Dict]:
130
    """
131
    Generates and returns a list of metrics for the given tag trades and the results dataframe
132
    :param starting_balance: Starting balance
133
    :param results: Dataframe containing the backtest results
134
    :param skip_nan: Print "left open" open trades
135
    :return: List of Dicts containing the metrics per pair
136
    """
137

138
    tabular_data = []
1✔
139

140
    if tag_type in results.columns:
1✔
141
        for tag, count in results[tag_type].value_counts().items():
1✔
142
            result = results[results[tag_type] == tag]
1✔
143
            if skip_nan and result['profit_abs'].isnull().all():
1✔
144
                continue
×
145

146
            tabular_data.append(_generate_result_line(result, starting_balance, tag))
1✔
147

148
        # Sort by total profit %:
149
        tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
1✔
150

151
        # Append Total
152
        tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
1✔
153
        return tabular_data
1✔
154
    else:
155
        return []
1✔
156

157

158
def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]:
1✔
159
    """
160
    Generate summary per strategy
161
    :param bt_stats: Dict of <Strategyname: DataFrame> containing results for all strategies
162
    :return: List of Dicts containing the metrics per Strategy
163
    """
164

165
    tabular_data = []
1✔
166
    for strategy, result in bt_stats.items():
1✔
167
        tabular_data.append(deepcopy(result['results_per_pair'][-1]))
1✔
168
        # Update "key" to strategy (results_per_pair has it as "Total").
169
        tabular_data[-1]['key'] = strategy
1✔
170
        tabular_data[-1]['max_drawdown_account'] = result['max_drawdown_account']
1✔
171
        tabular_data[-1]['max_drawdown_abs'] = fmt_coin(
1✔
172
            result['max_drawdown_abs'], result['stake_currency'], False)
173
    return tabular_data
1✔
174

175

176
def _get_resample_from_period(period: str) -> str:
1✔
177
    if period == 'day':
1✔
178
        return '1d'
1✔
179
    if period == 'week':
1✔
180
        # Weekly defaulting to Monday.
181
        return '1W-MON'
1✔
182
    if period == 'month':
1✔
183
        return '1ME'
1✔
184
    raise ValueError(f"Period {period} is not supported.")
1✔
185

186

187
def generate_periodic_breakdown_stats(
1✔
188
        trade_list: Union[List,  DataFrame], period: str) -> List[Dict[str, Any]]:
189

190
    results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list)
1✔
191
    if len(results) == 0:
1✔
192
        return []
1✔
193
    results['close_date'] = to_datetime(results['close_date'], utc=True)
1✔
194
    resample_period = _get_resample_from_period(period)
1✔
195
    resampled = results.resample(resample_period, on='close_date')
1✔
196
    stats = []
1✔
197
    for name, day in resampled:
1✔
198
        profit_abs = day['profit_abs'].sum().round(10)
1✔
199
        wins = sum(day['profit_abs'] > 0)
1✔
200
        draws = sum(day['profit_abs'] == 0)
1✔
201
        loses = sum(day['profit_abs'] < 0)
1✔
202
        trades = (wins + draws + loses)
1✔
203
        stats.append(
1✔
204
            {
205
                'date': name.strftime('%d/%m/%Y'),
206
                'date_ts': int(name.to_pydatetime().timestamp() * 1000),
207
                'profit_abs': profit_abs,
208
                'wins': wins,
209
                'draws': draws,
210
                'loses': loses,
211
                'winrate': wins / trades if trades else 0.0,
212
            }
213
        )
214
    return stats
1✔
215

216

217
def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]:
1✔
218
    result = {}
1✔
219
    for period in BACKTEST_BREAKDOWNS:
1✔
220
        result[period] = generate_periodic_breakdown_stats(trade_list, period)
1✔
221
    return result
1✔
222

223

224
def calc_streak(dataframe: DataFrame) -> Tuple[int, int]:
1✔
225
    """
226
    Calculate consecutive win and loss streaks
227
    :param dataframe: Dataframe containing the trades dataframe, with profit_ratio column
228
    :return: Tuple containing consecutive wins and losses
229
    """
230

231
    df = Series(np.where(dataframe['profit_ratio'] > 0, 'win', 'loss')).to_frame('result')
1✔
232
    df['streaks'] = df['result'].ne(df['result'].shift()).cumsum().rename('streaks')
1✔
233
    df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1
1✔
234
    res = df.groupby(df['result']).max()
1✔
235
    #
236
    cons_wins = int(res.loc['win', 'counter']) if 'win' in res.index else 0
1✔
237
    cons_losses = int(res.loc['loss', 'counter']) if 'loss' in res.index else 0
1✔
238
    return cons_wins, cons_losses
1✔
239

240

241
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
1✔
242
    """ Generate overall trade statistics """
243
    if len(results) == 0:
1✔
244
        return {
1✔
245
            'wins': 0,
246
            'losses': 0,
247
            'draws': 0,
248
            'winrate': 0,
249
            'holding_avg': timedelta(),
250
            'winner_holding_avg': timedelta(),
251
            'loser_holding_avg': timedelta(),
252
            'max_consecutive_wins': 0,
253
            'max_consecutive_losses': 0,
254
        }
255

256
    winning_trades = results.loc[results['profit_ratio'] > 0]
1✔
257
    draw_trades = results.loc[results['profit_ratio'] == 0]
1✔
258
    losing_trades = results.loc[results['profit_ratio'] < 0]
1✔
259

260
    holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
1✔
261
                   if not results.empty else timedelta())
262
    winner_holding_avg = (timedelta(minutes=round(winning_trades['trade_duration'].mean()))
1✔
263
                          if not winning_trades.empty else timedelta())
264
    loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
1✔
265
                         if not losing_trades.empty else timedelta())
266
    winstreak, loss_streak = calc_streak(results)
1✔
267

268
    return {
1✔
269
        'wins': len(winning_trades),
270
        'losses': len(losing_trades),
271
        'draws': len(draw_trades),
272
        'winrate': len(winning_trades) / len(results) if len(results) else 0.0,
273
        'holding_avg': holding_avg,
274
        'holding_avg_s': holding_avg.total_seconds(),
275
        'winner_holding_avg': winner_holding_avg,
276
        'winner_holding_avg_s': winner_holding_avg.total_seconds(),
277
        'loser_holding_avg': loser_holding_avg,
278
        'loser_holding_avg_s': loser_holding_avg.total_seconds(),
279
        'max_consecutive_wins': winstreak,
280
        'max_consecutive_losses': loss_streak,
281
    }
282

283

284
def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
1✔
285
    """ Generate daily statistics """
286
    if len(results) == 0:
1✔
287
        return {
1✔
288
            'backtest_best_day': 0,
289
            'backtest_worst_day': 0,
290
            'backtest_best_day_abs': 0,
291
            'backtest_worst_day_abs': 0,
292
            'winning_days': 0,
293
            'draw_days': 0,
294
            'losing_days': 0,
295
            'daily_profit_list': [],
296
        }
297
    daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
1✔
298
    daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
1✔
299
    worst_rel = min(daily_profit_rel)
1✔
300
    best_rel = max(daily_profit_rel)
1✔
301
    worst = min(daily_profit)
1✔
302
    best = max(daily_profit)
1✔
303
    winning_days = sum(daily_profit > 0)
1✔
304
    draw_days = sum(daily_profit == 0)
1✔
305
    losing_days = sum(daily_profit < 0)
1✔
306
    daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.items()]
1✔
307

308
    return {
1✔
309
        'backtest_best_day': best_rel,
310
        'backtest_worst_day': worst_rel,
311
        'backtest_best_day_abs': best,
312
        'backtest_worst_day_abs': worst,
313
        'winning_days': winning_days,
314
        'draw_days': draw_days,
315
        'losing_days': losing_days,
316
        'daily_profit': daily_profit_list,
317
    }
318

319

320
def generate_strategy_stats(pairlist: List[str],
1✔
321
                            strategy: str,
322
                            content: Dict[str, Any],
323
                            min_date: datetime, max_date: datetime,
324
                            market_change: float,
325
                            is_hyperopt: bool = False,
326
                            ) -> Dict[str, Any]:
327
    """
328
    :param pairlist: List of pairs to backtest
329
    :param strategy: Strategy name
330
    :param content: Backtest result data in the format:
331
                    {'results: results, 'config: config}}.
332
    :param min_date: Backtest start date
333
    :param max_date: Backtest end date
334
    :param market_change: float indicating the market change
335
    :return: Dictionary containing results per strategy and a strategy summary.
336
    """
337
    results: Dict[str, DataFrame] = content['results']
1✔
338
    if not isinstance(results, DataFrame):
1✔
339
        return {}
×
340
    config = content['config']
1✔
341
    max_open_trades = min(config['max_open_trades'], len(pairlist))
1✔
342
    start_balance = config['dry_run_wallet']
1✔
343
    stake_currency = config['stake_currency']
1✔
344

345
    pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
1✔
346
                                         starting_balance=start_balance,
347
                                         results=results, skip_nan=False)
348

349
    enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
1✔
350
                                             results=results, skip_nan=False)
351
    exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance,
1✔
352
                                             results=results, skip_nan=False)
353
    left_open_results = generate_pair_metrics(
1✔
354
        pairlist, stake_currency=stake_currency, starting_balance=start_balance,
355
        results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True)
356

357
    daily_stats = generate_daily_stats(results)
1✔
358
    trade_stats = generate_trading_stats(results)
1✔
359

360
    periodic_breakdown = {}
1✔
361
    if not is_hyperopt:
1✔
362
        periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)}
1✔
363

364
    best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
1✔
365
                    key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
366
    worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
1✔
367
                     key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
368
    winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
1✔
369
    losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
1✔
370
    profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
1✔
371

372
    expectancy, expectancy_ratio = calculate_expectancy(results)
1✔
373
    backtest_days = (max_date - min_date).days or 1
1✔
374
    strat_stats = {
1✔
375
        'trades': results.to_dict(orient='records'),
376
        'locks': [lock.to_json() for lock in content['locks']],
377
        'best_pair': best_pair,
378
        'worst_pair': worst_pair,
379
        'results_per_pair': pair_results,
380
        'results_per_enter_tag': enter_tag_results,
381
        'exit_reason_summary': exit_reason_stats,
382
        'left_open_trades': left_open_results,
383

384
        'total_trades': len(results),
385
        'trade_count_long': len(results.loc[~results['is_short']]),
386
        'trade_count_short': len(results.loc[results['is_short']]),
387
        'total_volume': float(results['stake_amount'].sum()),
388
        'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
389
        'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
390
        'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
391
        'profit_total': results['profit_abs'].sum() / start_balance,
392
        'profit_total_long': results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance,
393
        'profit_total_short': results.loc[results['is_short'], 'profit_abs'].sum() / start_balance,
394
        'profit_total_abs': results['profit_abs'].sum(),
395
        'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
396
        'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
397
        'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
398
        'expectancy': expectancy,
399
        'expectancy_ratio': expectancy_ratio,
400
        'sortino': calculate_sortino(results, min_date, max_date, start_balance),
401
        'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
402
        'calmar': calculate_calmar(results, min_date, max_date, start_balance),
403
        'profit_factor': profit_factor,
404
        'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
405
        'backtest_start_ts': int(min_date.timestamp() * 1000),
406
        'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
407
        'backtest_end_ts': int(max_date.timestamp() * 1000),
408
        'backtest_days': backtest_days,
409

410
        'backtest_run_start_ts': content['backtest_start_time'],
411
        'backtest_run_end_ts': content['backtest_end_time'],
412

413
        'trades_per_day': round(len(results) / backtest_days, 2),
414
        'market_change': market_change,
415
        'pairlist': pairlist,
416
        'stake_amount': config['stake_amount'],
417
        'stake_currency': config['stake_currency'],
418
        'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
419
        'starting_balance': start_balance,
420
        'dry_run_wallet': start_balance,
421
        'final_balance': content['final_balance'],
422
        'rejected_signals': content['rejected_signals'],
423
        'timedout_entry_orders': content['timedout_entry_orders'],
424
        'timedout_exit_orders': content['timedout_exit_orders'],
425
        'canceled_trade_entries': content['canceled_trade_entries'],
426
        'canceled_entry_orders': content['canceled_entry_orders'],
427
        'replaced_entry_orders': content['replaced_entry_orders'],
428
        'max_open_trades': max_open_trades,
429
        'max_open_trades_setting': (config['max_open_trades']
430
                                    if config['max_open_trades'] != float('inf') else -1),
431
        'timeframe': config['timeframe'],
432
        'timeframe_detail': config.get('timeframe_detail', ''),
433
        'timerange': config.get('timerange', ''),
434
        'enable_protections': config.get('enable_protections', False),
435
        'strategy_name': strategy,
436
        # Parameters relevant for backtesting
437
        'stoploss': config['stoploss'],
438
        'trailing_stop': config.get('trailing_stop', False),
439
        'trailing_stop_positive': config.get('trailing_stop_positive'),
440
        'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
441
        'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
442
        'use_custom_stoploss': config.get('use_custom_stoploss', False),
443
        'minimal_roi': config['minimal_roi'],
444
        'use_exit_signal': config['use_exit_signal'],
445
        'exit_profit_only': config['exit_profit_only'],
446
        'exit_profit_offset': config['exit_profit_offset'],
447
        'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'],
448
        **periodic_breakdown,
449
        **daily_stats,
450
        **trade_stats
451
    }
452

453
    try:
1✔
454
        max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
1✔
455
            results, value_col='profit_ratio')
456
        (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
1✔
457
         max_drawdown) = calculate_max_drawdown(
458
             results, value_col='profit_abs', starting_balance=start_balance)
459
        # max_relative_drawdown = Underwater
460
        (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
1✔
461
             results, value_col='profit_abs', starting_balance=start_balance, relative=True)
462

463
        strat_stats.update({
1✔
464
            'max_drawdown': max_drawdown_legacy,  # Deprecated - do not use
465
            'max_drawdown_account': max_drawdown,
466
            'max_relative_drawdown': max_relative_drawdown,
467
            'max_drawdown_abs': drawdown_abs,
468
            'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
469
            'drawdown_start_ts': drawdown_start.timestamp() * 1000,
470
            'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
471
            'drawdown_end_ts': drawdown_end.timestamp() * 1000,
472

473
            'max_drawdown_low': low_val,
474
            'max_drawdown_high': high_val,
475
        })
476

477
        csum_min, csum_max = calculate_csum(results, start_balance)
1✔
478
        strat_stats.update({
1✔
479
            'csum_min': csum_min,
480
            'csum_max': csum_max
481
        })
482

483
    except ValueError:
1✔
484
        strat_stats.update({
1✔
485
            'max_drawdown': 0.0,
486
            'max_drawdown_account': 0.0,
487
            'max_relative_drawdown': 0.0,
488
            'max_drawdown_abs': 0.0,
489
            'max_drawdown_low': 0.0,
490
            'max_drawdown_high': 0.0,
491
            'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
492
            'drawdown_start_ts': 0,
493
            'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
494
            'drawdown_end_ts': 0,
495
            'csum_min': 0,
496
            'csum_max': 0
497
        })
498

499
    return strat_stats
1✔
500

501

502
def generate_backtest_stats(btdata: Dict[str, DataFrame],
1✔
503
                            all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
504
                            min_date: datetime, max_date: datetime
505
                            ) -> BacktestResultType:
506
    """
507
    :param btdata: Backtest data
508
    :param all_results: backtest result - dictionary in the form:
509
                     { Strategy: {'results: results, 'config: config}}.
510
    :param min_date: Backtest start date
511
    :param max_date: Backtest end date
512
    :return: Dictionary containing results per strategy and a strategy summary.
513
    """
514
    result: BacktestResultType = {
1✔
515
        'metadata': {},
516
        'strategy': {},
517
        'strategy_comparison': [],
518
    }
519
    market_change = calculate_market_change(btdata, 'close')
1✔
520
    metadata = {}
1✔
521
    pairlist = list(btdata.keys())
1✔
522
    for strategy, content in all_results.items():
1✔
523
        strat_stats = generate_strategy_stats(pairlist, strategy, content,
1✔
524
                                              min_date, max_date, market_change=market_change)
525
        metadata[strategy] = {
1✔
526
            'run_id': content['run_id'],
527
            'backtest_start_time': content['backtest_start_time'],
528
            'timeframe': content['config']['timeframe'],
529
            'timeframe_detail': content['config'].get('timeframe_detail', None),
530
            'backtest_start_ts': int(min_date.timestamp()),
531
            'backtest_end_ts': int(max_date.timestamp()),
532
        }
533
        result['strategy'][strategy] = strat_stats
1✔
534

535
    strategy_results = generate_strategy_comparison(bt_stats=result['strategy'])
1✔
536

537
    result['metadata'] = metadata
1✔
538
    result['strategy_comparison'] = strategy_results
1✔
539

540
    return result
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc